您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Improves the display of servers on otservlist.org. Example features include filtering, sorting, hiding ads, and displaying all servers on one large page.
/******/ (() => { // webpackBootstrap /******/ "use strict"; ;// ./src/Core.ts const debugMode = true; function debug(message) { // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (debugMode) console.log(message); } /** Inherit from this class if you want to ensure your type won't be assignable to other types with identical structure (and vice-versa). */ class Distinct { } function warnAlert(message) { message = `Better otservlist error:\n${message}`; console.warn(message); alert(message); } function as(obj, type) { return obj instanceof type ? obj : undefined; } function cast(obj, type) { if (obj instanceof type) return obj; else throw new Error(`Could not cast an object of type ${typeof (obj)} to ${type.constructor.name}.`); } function assertNotNull(obj, errorMessage = "Object is null!") { if (obj === null) throw new Error(errorMessage); else return obj; } function assertDefined(obj, errorMessage = "Object is undefined!") { if (obj === undefined) throw new Error(errorMessage); else return obj; } function ofType(items, type) { const filteredItems = []; for (const item of items) { if (item instanceof type) filteredItems.push(item); } return filteredItems; } function withoutUndefined(items) { const filteredItems = []; for (const item of items) { if (item !== undefined) filteredItems.push(item); } return filteredItems; } function withoutNulls(items) { const filteredItems = []; for (const item of items) { if (item !== null) filteredItems.push(item); } return filteredItems; } function safeQuerySelector(node, selector) { const foundElement = node.querySelector(selector); if (foundElement === null) throw new Error(`${selector} not found!`); else return foundElement; } function tryParseInt(str) { const num = parseInt(str); return isNaN(num) ? undefined : num; } function tryParseFloat(str) { const num = parseFloat(str); return isNaN(num) ? undefined : num; } function tryParseNullableFloat(str) { return str === "" ? null : tryParseFloat(str); } function asString(number) { return number?.toString() ?? ""; } function edit(obj, action) { action(obj); return obj; } function last(array) { return array[array.length - 1]; } function lazy(getter) { let value; let isValueCreated = false; return () => { if (!isValueCreated) { value = getter(); isValueCreated = true; } return value; }; } /** min/max == null: any non-null value is fine | value == null: value never fulfills condition, unless both min and max are null */ function withinRange(valueFunc, minFunc, maxFunc) { let value, min, max; return ((min = minFunc()) === null || ((value = valueFunc()) !== null && min <= value)) && ((max = maxFunc()) === null || ((value = valueFunc()) !== null && value <= max)); } ;// ./src/UI.ts function withClass(element, className) { element.className = className; return element; } function addStyle(cssCode) { const style = document.createElement("style"); style.type = "text/css"; style.innerHTML = `${cssCode}`; assertDefined(document.getElementsByTagName("head")[0]).appendChild(style); return style; } function div(...nodes) { const div = document.createElement("div"); div.append(...withoutUndefined(nodes)); return div; } function label(text, content) { const label = document.createElement("label"); label.textContent = text; if (content !== undefined) label.insertBefore(content, label.firstChild); return label; } function UI_checkbox(boolProperty, settingApplier) { const checkbox = document.createElement("input"); checkbox.type = "checkbox"; checkbox.checked = boolProperty.get(); checkbox.addEventListener("change", () => { boolProperty.set(checkbox.checked); settingApplier.apply(); }); return checkbox; } function numberInput(numberProperty, settingApplier) { const numberInput = document.createElement("input"); numberInput.type = "number"; numberInput.min = "0"; numberInput.size = 8; numberInput.value = asString(numberProperty.get()); numberInput.addEventListener("change", () => { const number = assertDefined(tryParseNullableFloat(numberInput.value)); numberProperty.set(number); settingApplier.apply(); }); return numberInput; } function enumSingleSelect(enum_, enumProperty, settingApplier) { const singleSelect = document.createElement("select"); const chosenValue = enumProperty.get(); for (const enumValue of enum_.all()) { const option = new Option( /* text: */ enumValue.description, /* value: */ enumValue.id, /* defaultSelected: */ undefined, /* selected: */ enumValue == chosenValue); singleSelect.add(option); } singleSelect.addEventListener("change", () => { const enumValue = assertDefined(enum_.byId(singleSelect.value)); enumProperty.set(enumValue); settingApplier.apply(); }); return singleSelect; } ;// ./src/Enum.ts class EnumValue extends Distinct { constructor(id, description) { super(); this.id = id; this.description = description; } } class Enum { constructor(type) { this._dictionary = null; this._type = type; } /** Key is the enum value's id, not property's name! */ _getDictionary() { if (this._dictionary === null) { this._dictionary = {}; const values = ofType(Object.values(this), this._type); for (const value of values) this._dictionary[value.id] = value; } return this._dictionary; } all() { return Object.values(this._getDictionary()); } byId(id) { return this._getDictionary()[id]; } } ;// ./src/ServerSorts.ts class ServerPropertyForSorting { constructor(getter, highestFirst) { this._getter = getter; this._highestFirst = highestFirst; } /** -1: show s1 first, 1: show s2 first */ compareFn() { return (s1, s2) => { const s1Value = this._getter(s1); const s2Value = this._getter(s2); let result; if (s1Value > s2Value) result = this._highestFirst ? -1 : 1; else if (s1Value < s2Value) result = this._highestFirst ? 1 : -1; else result = 0; return result; }; } } class ServerSort extends EnumValue { constructor(id, description, propertyForSorting) { super(id, description); this.propertyForSorting = propertyForSorting; } } class _ServerSorts extends Enum { constructor() { super(...arguments); this.Default = new ServerSort("Default", "(default)", null); this.DateAddedNewestFirst = new ServerSort("DateAddedNewestFirst", "Date added (newest first)", new ServerPropertyForSorting(s => s.id(), true)); this.DateAddedOldestFirst = new ServerSort("DateAddedOldestFirst", "Date added (oldest first)", new ServerPropertyForSorting(s => s.id(), false)); this.ExpRateHighestFirst = new ServerSort("ExpRateHighestFirst", "Exp rate (highest first)", new ServerPropertyForSorting(s => s.expRate(), true)); this.ExpRateLowestFirst = new ServerSort("ExpRateLowestFirst", "Exp rate (lowest first)", new ServerPropertyForSorting(s => s.expRate(), false)); this.PlayerCountNowHighestFirst = new ServerSort("PlayerCountNowHighestFirst", "Players now (highest first)", new ServerPropertyForSorting(s => s.playerCount().now ?? -1, true)); this.PlayerCountNowLowestFirst = new ServerSort("PlayerCountNowLowestFirst", "Players now (lowest first)", new ServerPropertyForSorting(s => s.playerCount().now ?? -1, false)); this.UptimeHighestFirst = new ServerSort("UptimeHighestFirst", "Uptime (highest first)", new ServerPropertyForSorting(s => s.uptimePercent(), true)); this.UptimeLowestFirst = new ServerSort("UptimeLowestFirst", "Uptime (lowest first)", new ServerPropertyForSorting(s => s.uptimePercent(), false)); this.PointsHighestFirst = new ServerSort("PointsHighestFirst", "Points (highest first)", new ServerPropertyForSorting(s => s.points() ?? -1, true)); this.PointsLowestFirst = new ServerSort("PointsLowestFirst", "Points (lowest first)", new ServerPropertyForSorting(s => s.points() ?? 1, false)); this.VersionNewestFirst = new ServerSort("VersionNewestFirst", "Version (newest first)", new ServerPropertyForSorting(s => s.clientVersion() ?? -1, true)); this.VersionOldestFirst = new ServerSort("VersionOldestFirst", "Version (oldest first)", new ServerPropertyForSorting(s => s.clientVersion() ?? -1, false)); } } const ServerSorts = new _ServerSorts(ServerSort); /* harmony default export */ const src_ServerSorts = (ServerSorts); ;// ./src/Settings.ts class StoredVariable { constructor(key, defaultValue) { //if (this._allKeys.has(key)) // throw new Error(`There is already a StoredVariable with key "${key}"!`); //this._allKeys.add(key); /** @readonly */ this.key = key; /** @readonly */ this.defaultValue = defaultValue; } //private readonly _allKeys: Set<string> = new Set<string>(); get() { const storedString = localStorage.getItem(this.key); if (storedString === null) // No stored value return this.defaultValue; const deserializedValue = this.deserialize(storedString); if (deserializedValue === undefined) // Stored string couldn't be deserialized. { console.warn(`Found value "${storedString}" for ${this.key}, but it couldn't be deserialized. Returning default value.`); return this.defaultValue; } else return deserializedValue; } set(value) { localStorage.setItem(this.key, this.serialize(value)); } } class StoredBool extends StoredVariable { serialize(value) { return value ? "1" : "0"; } deserialize(storedString) { return (storedString === "1" ? true : storedString === "0" ? false // Else : undefined); } } class StoredNumber extends StoredVariable { serialize(value) { return asString(value); } deserialize(storedString) { return tryParseNullableFloat(storedString); } } class StoredEnum extends StoredVariable { constructor(enum_, key, defaultValue) { super(key, defaultValue); this._enum = enum_; } serialize(value) { return value.id; } deserialize(storedString) { return this._enum.byId(storedString); } } class Settings { } Settings.hideAds = new StoredBool("hideAds", false); Settings.unpromoteServers = new StoredBool("unpromoteServer", false); // Hides "Starting soon", "Show promoted servers" and removes the yellow highlight from promoted servers. Settings.showOfflineServers = new StoredBool("showOfflineServers", true); Settings.minExpRate = new StoredNumber("minExpRate", null); Settings.maxExpRate = new StoredNumber("maxExpRate", null); Settings.minVersion = new StoredNumber("minVersion", null); Settings.maxVersion = new StoredNumber("maxVersion", null); Settings.minPlayerCountNow = new StoredNumber("minPlayerCountNow", null); Settings.maxPlayerCountNow = new StoredNumber("maxPlayerCountNow", null); Settings.sortBy = new StoredEnum(src_ServerSorts, "sortBy", src_ServerSorts.Default); ;// ./src/SettingAppliers.ts class SettingApplier { constructor() { this.hiddenElements = []; } hide(htmlElement) { if (htmlElement !== null && htmlElement !== undefined) { htmlElement.hidden = true; this.hiddenElements.push(htmlElement); } } hideIfHtmlElement(element) { if (element instanceof HTMLElement) this.hide(element); } undoHidingElements() { for (const element of this.hiddenElements) element.hidden = false; this.hiddenElements = []; } } class HideAds extends SettingApplier { apply() { if (Settings.hideAds.get()) { // All pages this.hideIfHtmlElement(document.querySelector("#homeboxes")); // Servlist pages this.hideIfHtmlElement(document.querySelector("#banner_cont")); // Specific server pages const serverAboutTable = as(document.querySelector("#serverabout"), HTMLTableElement); if (serverAboutTable !== undefined) { const probablyAdBannerRow = as(serverAboutTable.tBodies[0]?.children[0], HTMLTableRowElement); const probablyAdBannerLink = as(probablyAdBannerRow?.querySelector("a"), HTMLAnchorElement); if (probablyAdBannerLink?.href.includes("/adv/") === true) this.hide(probablyAdBannerRow); document.querySelectorAll("#summary_banner").forEach(banner => { this.hideIfHtmlElement(banner); }); const serverIpLink = as(document.querySelector("#content")?.querySelector(".servname")?.querySelector("a"), HTMLAnchorElement); if (serverIpLink !== undefined) { debug("serverIpLink"); debug(serverIpLink.text); const ip = serverIpLink.text.match(/(.*):/)?.[1]; if (ip !== undefined) serverIpLink.href = `http://${ip}`; } } } else this.undoHidingElements(); } } class UnpromoteServers extends SettingApplier { constructor(servers) { super(); this._unpromotedServers = []; this._servers = servers; } apply() { if (Settings.unpromoteServers.get()) { const contentChildren = document.querySelector("#content")?.children; if (contentChildren === undefined) { console.warn("Unpromoting servers failed because an element with id 'content' wasn't found."); return; } const startingSoonServlist = as(contentChildren[0], HTMLTableElement); const firstBr = as(contentChildren[1], HTMLBRElement); const secondBr = as(contentChildren[2], HTMLBRElement); const showPromotedServersBar = as(contentChildren[3], HTMLDivElement); const thirdBr = as(contentChildren[4], HTMLBRElement); if (startingSoonServlist !== undefined && startingSoonServlist.id == "servlist") { this.hide(startingSoonServlist); this.hide(firstBr); this.hide(secondBr); } if (showPromotedServersBar !== undefined && showPromotedServersBar.className == "promoted_bar") { this.hide(showPromotedServersBar); this.hide(thirdBr); } for (const promotedServer of this._servers.filter(s => s.element.id == "prom")) { promotedServer.element.id = "s"; this._unpromotedServers.push(promotedServer); } } else { this.undoHidingElements(); for (const unpromotedServer of this._unpromotedServers) unpromotedServer.element.id = "prom"; this._unpromotedServers = []; } } } class FilterAndSortServers extends SettingApplier { constructor(servers, mainServlist) { super(); this._servers = servers; this._mainServlist = mainServlist; } apply() { const filteredServers = []; const minExpRate = lazy(() => Settings.minExpRate.get()); const maxExpRate = lazy(() => Settings.maxExpRate.get()); const minVersion = lazy(() => Settings.minVersion.get()); const maxVersion = lazy(() => Settings.maxVersion.get()); const minPlayerCountNow = lazy(() => Settings.minPlayerCountNow.get()); const maxPlayerCountNow = lazy(() => Settings.maxPlayerCountNow.get()); const showOfflineServers = lazy(() => Settings.showOfflineServers.get()); for (const server of this._servers) { const criteriaFulfilled = withinRange(server.expRate, minExpRate, maxExpRate) && withinRange(server.clientVersion, minVersion, maxVersion) && withinRange(() => server.playerCount().now, minPlayerCountNow, maxPlayerCountNow) && (showOfflineServers() || server.isOnline()); if (criteriaFulfilled) filteredServers.push(server); } const propertyForSorting = Settings.sortBy.get().propertyForSorting; const mainServlistBody = safeQuerySelector(this._mainServlist, "tbody"); const mainServlistHeader = assertNotNull(mainServlistBody.firstChild); let newServers = filteredServers; if (propertyForSorting !== null) newServers = newServers.sort(propertyForSorting.compareFn()); mainServlistBody.replaceChildren(...[mainServlistHeader].concat(newServers.map(server => server.element))); } } ;// ./src/Server.ts class PlayerCount { constructor(now, highestEver, limit) { this.now = now; this.highestEver = highestEver; this.limit = limit; } } class Server { constructor(serverElement) { //let flagUrl = server.children[0].querySelector('img').src; //let ip = server.children[1].childNodes[0].text; this.id = lazy(() => this.parseServerProperty("otservlistUrl", cast(this.element.children[1]?.firstChild, HTMLAnchorElement).href, // e.g. "https://otservlist.org/ots/1234567" // e.g. "https://otservlist.org/ots/1234567" str => tryParseInt(last(str.split("ots/")) ?? ""))); // e.g. 1234567 // childNodes[2] isn't useful //let serverName = server.children[3].childNodes[0].data; this.playerCount = lazy(() => this.parseServerProperty("playerCountString", this.element.children[4]?.textContent, // e.g. "8 (13) / 100" or "?? (13) / ??" // e.g. "8 (13) / 100" or "?? (13) / ??" str => { const regExpMatchArray = str.match(/(\d+|\?\?) \((\d+)\) \/ (\d+|\?\?)/); // We skip result 0 because it contains entire string. const nowString = regExpMatchArray?.[1]; // e.g. "8" or "??" const highestEverString = regExpMatchArray?.[2]; // e.g. "13" const limitString = regExpMatchArray?.[3]; // e.g. "100" or "??" if (nowString === undefined || highestEverString === undefined || limitString === undefined) return undefined; const now = nowString == "??" ? null : tryParseInt(nowString); const highestEver = tryParseInt(highestEverString); const limit = limitString == "??" ? null : tryParseInt(limitString); if (now === undefined || highestEver === undefined || limit === undefined) return undefined; return new PlayerCount(now, highestEver, limit); })); this.uptimePercent = lazy(() => this.parseServerProperty("uptimeString", this.element.children[5]?.textContent, // e.g. "99.80%" // e.g. "99.80%" str => tryParseFloat(str))); // e.g. 99.80 this.points = lazy(() => this.parseServerProperty("pointsString", this.element.children[6]?.textContent, // e.g. "120" or "??" // e.g. "120" or "??" str => tryParseInt(str) ?? (str == "??" ? null : undefined))); // e.g. 120 or null this.expRate = lazy(() => this.parseServerProperty("expRateString", this.element.children[7]?.textContent, // e.g. "x1.5" // e.g. "x1.5" str => tryParseFloat(str.substring(1)))); // e.g. 1.5) //let serverType = server.children[8].childNodes[0].data; this.clientVersion = lazy(() => this.parseServerProperty("clientVersionString", this.element.children[9]?.textContent, // e.g. "[ 8.54 ]" or "[ n/a ]" // e.g. "[ 8.54 ]" or "[ n/a ]" str => { const innerClientVersionString = str.slice(2, -2); // e.g. "8.54" or "n/a" // return: e.g. 8.54 or null return innerClientVersionString == "n/a" ? null : tryParseFloat(innerClientVersionString); })); this.element = serverElement; } isOnline() { return this.points() !== null; } parseServerProperty(stringToParseName, stringToParse, tryParseAction) { stringToParse = assertNotNull(assertDefined(stringToParse)); return assertDefined(tryParseAction(stringToParse), `${stringToParseName} = "${stringToParse}": Incorrect format!`); } } /* harmony default export */ const src_Server = (Server); ;// ./src/_main.ts // ==UserScript== // @name Better otservlist.org // @namespace BetterOtservlist // @version 0.7.6 // @description Improves the display of servers on otservlist.org. Example features include filtering, sorting, hiding ads, and displaying all servers on one large page. // @author Wirox // @match https://otservlist.org/* // @match https://*.otservlist.org/* // @match http://otservlist.org/* // @match http://*.otservlist.org/* // @grant none // ==/UserScript== // eslint-disable-next-line @typescript-eslint/no-floating-promises (async function () { try { await main(); } catch (error) { const errorMessage = error instanceof Error ? `BetterOtservlist error:\n${error.message}` : "BetterOtservlist error:\nUnknown error"; alert(errorMessage); throw error; } })(); async function main() { const hideAds = new HideAds(); hideAds.apply(); const servlistsOnThisPage = Array.from(document.querySelectorAll("#servlist")); const mainServlist = last(servlistsOnThisPage); // We get the last servList because if there are two, the first one is only promotional. if (mainServlist === undefined) return; // If there are no servlists, the plugin isn't supposed to do anything besides possibly hiding ads. // If the search includes the 'allServers' parameter, we transform it into a page with all servers. const showAllServers = new URLSearchParams(window.location.search).has("allServers"); if (showAllServers) { document.title = "otservlist.org - All servers"; safeQuerySelector(safeQuerySelector(document, "#content"), "#title").textContent = "All servers"; const mainServlistBody = safeQuerySelector(mainServlist, "tbody"); mainServlistBody.replaceChildren(assertNotNull(mainServlistBody.firstChild)); // We show an empty list (first child is the header) while we load all servers. } let loader; if (showAllServers) { addStyle(` .loader { position: fixed; margin: auto; top: -10%; right: 0; bottom: 0; left: 0; z-index: 2; width: 80px; padding: 12px; aspect-ratio: 1; border-radius: 50%; background: #2a5872; --_m: conic-gradient(#0000 10%,#000), linear-gradient(#000 0 0) content-box; -webkit-mask: var(--_m); mask: var(--_m); -webkit-mask-composite: source-out; mask-composite: subtract; animation: l3 1s infinite linear; } @keyframes l3 {to{transform: rotate(1turn)}} `); loader = withClass(div(), "loader"); safeQuerySelector(document, "body").appendChild(loader); } const servlists = showAllServers ? await getAllServlists() : [mainServlist]; const serverElements = []; for (const servlist of servlists) { serverElements.push(...Array.from(servlist.querySelectorAll("#s"))); // Regular servers serverElements.push(...Array.from(servlist.querySelectorAll("#prom"))); // Promoted servers } const servers = serverElements.filter(e => e.children.length == 10).map(e => new src_Server(e)); const unpromoteServers = new UnpromoteServers(servers); unpromoteServers.apply(); const filterAndSortServers = new FilterAndSortServers(servers, mainServlist); filterAndSortServers.apply(); addUIBeforeElement(document.querySelector(".pager") ?? mainServlist, showAllServers, hideAds, unpromoteServers, filterAndSortServers); loader?.remove(); } function addUIBeforeElement(element, showAllServers, hideAds, unpromoteServers, filterAndSortServers) { const center = document.createElement("center"); const divs = []; const expRateDiv = div(label("Exp rate: "), numberInput(Settings.minExpRate, filterAndSortServers), " – ", numberInput(Settings.maxExpRate, filterAndSortServers)); divs.push(expRateDiv); const versionDiv = div(label("Version: "), numberInput(Settings.minVersion, filterAndSortServers), " – ", numberInput(Settings.maxVersion, filterAndSortServers)); divs.push(versionDiv); const playerCountNowDiv = div(label("Players now: "), numberInput(Settings.minPlayerCountNow, filterAndSortServers), " – ", numberInput(Settings.maxPlayerCountNow, filterAndSortServers)); divs.push(playerCountNowDiv); const sortByDiv = div(label("Sort by: "), enumSingleSelect(src_ServerSorts, Settings.sortBy, filterAndSortServers)); divs.push(sortByDiv); const checkboxesDiv = div(label("Hide ads", UI_checkbox(Settings.hideAds, hideAds)), label("Unpromote servers", UI_checkbox(Settings.unpromoteServers, unpromoteServers)), showAllServers ? label("Show offline servers", UI_checkbox(Settings.showOfflineServers, filterAndSortServers)) : undefined); divs.push(checkboxesDiv); if (!showAllServers) { const allServersLinkDiv = div(edit(document.createElement("a"), a => { a.href = "/search/5c7a8d09ac62611ea14b103e54b7a53b?allServers=true"; // Used to be 000[...] but for some reason that stopped working. a.textContent = "All servers"; })); divs.push(allServersLinkDiv); } center.append(...divs); element.insertAdjacentElement("beforebegin", center); } async function getAllServlists() { const domParser = new DOMParser(); const links = [ "/search/5c7a8d09ac62611ea14b103e54b7a53b", // PVP "/search/bde9f97e98ae103e3ba4317f3567f4f7", // nPVP "/search/f495a91e7b674d49a59594ba016d061e", // PVPe "/search/58aad7c689271c21017df46ca43f2388", // WAR "/search/32f3ac635aa8045e6f6241ea14019e85", // FUN ]; const promises = []; for (const link of links) { const promise = fetch(link).then(response => response.text()).catch(() => null); promises.push(promise); } return Promise.all(promises) .then(responseTexts => { const servLists = []; for (const responseText of withoutNulls(responseTexts)) servLists.push(...Array.from(domParser.parseFromString(responseText, "text/html").querySelectorAll("#servlist"))); if (responseTexts.filter(text => text === null).length > 0) warnAlert("Couldn't load some servers. Try refreshing the page."); return servLists; }) .catch(() => { warnAlert("Couldn't load any servers. Try refreshing the page."); return []; }); } /******/ })() ;