// ==UserScript==
// @name pttw
// @namespace https://nekit270.ch/pttw
// @version 1.7
// @description Добавляет новые функции в Pony Town.
// @author nekit270
// @match http://*.pony.town/*
// @match https://*.pony.town/*
// @icon https://google.com/s2/favicons?sz=64&domain=pony.town
// @grant none
// ==/UserScript==
(function(){
let w = (window.unsafeWindow?window.unsafeWindow:window);
if(self != top) return;
//[Глобальные переменные]
//Короткие имена для функций DOM
const qs = (s,e)=>(e??document).querySelector(s);
const qsa = (s,e)=>(e??document).querySelectorAll(s);
const ce = n=>document.createElement(n);
let twOptions, twScripts, gl, ws, onstartmenuloaded, ongameloaded;
//Object.merge
Object.prototype.merge = function(obj){
for(let i in obj){
let e = obj[i];
this[i] = e;
}
return this;
}
Object.defineProperty(Object.prototype, 'merge', {enumerable: false});
//Финт ушами, чтобы получать массив пикселей с экрана
HTMLCanvasElement.prototype.realGC = HTMLCanvasElement.prototype.getContext;
HTMLCanvasElement.prototype.getContext = function(c, o){
if(!o) o = {};
o.preserveDrawingBuffer = true;
let ctx = this.realGC(c, o);
if(c.includes('webgl')) gl = ctx;
return ctx;
}
const eventListeners = [];
EventTarget.prototype.realAEL = EventTarget.prototype.addEventListener;
EventTarget.prototype.addEventListener = function(t, l, o){
eventListeners.push({event: t, listener: l, target: this, options: o, useCapture: o});
this.realAEL(t, l, o);
}
//[/Глобальные переменные]
//[API]
//Вспомогательные классы для API
class Player{
constructor(elem, name, status, tags, social){
this.elem = elem;
this.name = name;
this.status = status;
this.tags = tags;
this.social = social;
}
isOpened(){
return !!this.elem.parentNode;
}
close(){
dispatchEvent(new KeyboardEvent('keydown', {keyCode: 27}));
}
action(act){
let pb = this.elem, menu = qs('div.dropdown-menu', pb), toggle = qs('button.dropdown-toggle', pb);
if(!menu){
toggle.click();
menu = qs('div.dropdown-menu', pb);
}
let ret = false;
qsa('button.dropdown-item', pb).forEach(btn=>{
if(btn.innerText.toLowerCase().includes(act.toLowerCase())){
btn.click();
toggle.click();
ret = true;
}
});
return ret;
}
isActionAvailable(act){
let pb = this.elem, menu = qs('div.dropdown-menu', pb), toggle = qs('button.dropdown-toggle', pb);
if(!menu){
toggle.click();
menu = qs('div.dropdown-menu', pb);
}
let ret = false;
qsa('button.dropdown-item', pb).forEach(btn=>{
if(btn.innerText.toLowerCase().includes(act.toLowerCase()) && !btn.disabled){
ret = true;
}
});
return ret;
}
}
class Message{
constructor(elem, type, time, author, text){
this.elem = elem;
this.type = type;
this.time = time;
this.author = author;
this.text = text;
}
getPlayer(){
return pt.player.getByMessage(this);
}
}
Message.create = function(text, author, type){
if(typeof author == 'undefined') author = 'system';
if(typeof type == 'undefined') type = '';
return new Message(null, type, getFormattedTime(), author, text);
}
//Объект API
const pt = {
keyCodes: {
left: 37,
up: 38,
right: 39,
down: 40
},
Player: Player,
Message: Message,
info(){
console.log('Pony Town Client API by nekit270 (https://github.com/nekit270)');
},
move(dir, time){
//передвинуть персонажа в направлении dir time секунд, затем вызвать callback
w.dispatchEvent(new KeyboardEvent('keydown', {keyCode: this.keyCodes[dir]}));
return new Promise((res, rej)=>{
setTimeout(()=>{
w.dispatchEvent(new KeyboardEvent('keyup', {keyCode: this.keyCodes[dir]}));
res();
}, time??150);
});
},
action(name){
//выполнить действие
let btn, btns = qsa('.action-button');
if(typeof name == 'string'){
//Передано название действия
btns.forEach(e=>{
if(!btn && e.title.toLowerCase().includes(name.toLowerCase())) btn = e;
});
}else if(typeof name == 'number'){
//Передан номер действия
if(name == -1){
pt.chat.sendMessage('/e');
return;
}
btn = btns[name];
}
if(!btn) throw new Error('Action not found');
btn.click();
},
zoom: {
set(n){
let settingsBtn = qs('div.settings-box button');
let style = ce('style').merge({ innerText: '.settings-box-menu{ position: absolute; width: 1px; height: 1px; left: -999px; top: -999px; }' });
document.body.appendChild(style);
if(!qs('div.settings-height')){
settingsBtn = qs('div.settings-box button');
settingsBtn.click();
}
let inBtn = qs(`button[aria-label="Zoom in"]`);
let outBtn = qs(`button[aria-label="Zoom out"]`);
for(let i = 0; i < 5; i++) outBtn.click();
let num = 0;
if(n <= 4) num = n - 1;
else num = n - 2;
for(let j = 0; j < num; j++) inBtn.click();
return new Promise((res, rej)=>{
setTimeout(()=>{
document.body.removeChild(style);
settingsBtn.click();
res();
}, 150);
});
},
get(){
let settingsBtn = qs('div.settings-box button');
let style = ce('style').merge({ innerText: '.settings-box-menu{ position: absolute; width: 1px; height: 1px; left: -999px; top: -999px; }' });
document.body.appendChild(style);
if(!qs('div.settings-height')){
settingsBtn = qs('div.settings-box button');
settingsBtn.click();
}
return new Promise((res, rej)=>{
setTimeout(()=>{
let z = parseInt(qs('div[title="Current zoom level"]').innerText.match(/Zoom ([0-9]{1})x/)[1]);
document.body.removeChild(style);
settingsBtn.click();
res(z);
}, 150);
});
}
},
chat: {
open(){
if(!qs('.chat-line')) qs('[title="Toggle chatlog"]').click();
},
getMessageByElement(msg){
let time = new Date(), timeArr = qs('.chat-line-timestamp', msg).innerText.split(':');
time.setHours(parseInt(timeArr[0]));
time.setMinutes(parseInt(timeArr[1]));
return new Message(
msg, //elem
(msg.className.replace(' chat-line-break', '').split(' ')[1]||'normal').replaceAll(' ', '').replace('chat-line', '').replace('-', ''), //type
time, //time
qs('.chat-line-name-content', msg).innerText, //author
qs('.chat-line-message', msg).innerText //text
);
},
getMessage(offset){
//Получение сообщения
let messages = qsa('.chat-line');
if(!messages || messages.length == 0){
//Сообщений нет, нужно открыть чат
qs('[title="Toggle chatlog"]').click();
messages = qsa('.chat-line');
}
let msg = messages[messages.length - ((offset??0)+1)];
if(!msg) return null;
return this.getMessageByElement(msg);
},
getMessages(start, end){
//Получение всех сообщений от start до end
let arr = [];
for(let i = (start??0); i < (end??100); i++){
let result = this.getMessage(i);
if(result) arr.push(result);
}
return arr;
},
sendMessage(text){
//Отправка сообщения
let chatBox = qs('.chat-box');
if(!chatBox || chatBox.getAttribute('hidden') === ''){
qs('[title="Toggle chat"]').click();
chatBox = qs('.chat-box');
}
let inp = qs('.chat-textarea');
inp.value = text;
//Обязательно нужно вызвать событие input, иначе не отправится
inp.dispatchEvent(new InputEvent('input'));
qs('[aria-label="Send message"]').click();
if(text.trim().length == 0) return;
pt.chat.disableReceive = true;
let mo = new MutationObserver(records=>{
records.forEach(record=>{
if(record.addedNodes.length > 0){
pt.chat.disableReceive = false;
mo.disconnect();
}
});
});
mo.observe(qs('.chat-log-scroll-inner'), { childList: true });
},
addMessage(msg){
//Добавляет сообщение в чат (только в чат, над игроком ничего отображено не будет)
if(typeof msg == 'string') msg = Message.create(msg);
let messages = qsa('.chat-line');
if(!messages || messages.length == 0){
qs('[title="Toggle chatlog"]').click();
messages = qsa('.chat-line');
}
let el = messages[messages.length - 1];
let nel = el.cloneNode(true);
el.parentNode.appendChild(nel);
qs('.chat-log-scroll-outer').scroll(0, Number.MAX_SAFE_INTEGER);
msg.elem = nel;
this.editMessage(msg);
return msg;
},
editMessage(msg){
//Редактирование сообщения
let el = msg.elem;
qs('.chat-line-name-content', el).innerText = msg.author;
qs('.chat-line-message', el).innerText = msg.text;
},
registerCommand(name, cb){
//Установка кастомной команды
this.commands[name] = cb;
},
logger: {
//Логгер чата
text: '',
observer: null,
isLogging: false,
start(){
//Запустить логгер
this.isLogging = true;
this.observer = new MutationObserver(r=>{
this.text += r[1].addedNodes[0].innerText+'\n';
});
this.observer.observe(qs('.chat-log-scroll-inner'), {childList: true});
},
stop(){
//Остановить логгер
this.isLogging = false;
this.observer.disconnect();
this.observer = null;
let text = this.text.toString();
this.text = '';
return text;
}
},
commands: [],
hook: {
send: [],
receive: [],
attach(type, func){
return this[type].push(func) - 1;
},
detach(type, index){
this[type].splice(index, 1);
}
}
},
status: {
get(){
//Получить статус
let status = qs('ui-button', qs('status-box')).title;
return status.split('|')[1].trim().toLowerCase();
},
set(status){
//Установить статус
let stBtn, btns;
btns = qsa('.status-dropdown-menu a.dropdown-item.mt-1');
if(!btns || btns.length == 0){
let statusBtn = qs('.status-button')
statusBtn.click();
setTimeout(()=>statusBtn.click(), 200);
btns = qsa('.status-dropdown-menu a.dropdown-item.mt-1');
}
btns.forEach(e=>{
if(e.innerText.toLowerCase().includes(status.toLowerCase())) stBtn = e;
});
if(!stBtn) throw new Error('Status not found.');
stBtn.click();
}
},
player: {
get(){
//Получение объекта Player из диалога игрока
let ponyBox = qs('pony-box');
if(!ponyBox) throw new Error('Pony box not found.');
let tags = [];
qsa('.pony-box-tag', ponyBox).forEach(e=>{
tags.push(e.innerText.toLowerCase());
});
let status = '', statusEl = qs('.pony-box-name-status', ponyBox);
if(statusEl.getAttribute('ngbtooltip')){
status = statusEl.getAttribute('ngbtooltip').replaceAll(' ', '-').toLowerCase();
}else{
status = statusEl.className.replace('ng-fa-icon pony-box-name-status text-', '').toLowerCase();
}
let social = { name: null, url: null }, socialEl = qs('site-info', ponyBox);
if(socialEl){
social.name = qs('.sr-only', socialEl)?.innerText?.trim()||null;
social.url = (qs('a', socialEl)?.href)??null;
}
return new Player(ponyBox, qs('.pony-box-name-text', ponyBox).innerText, status, tags, social);
},
getByMessage(msg){
//Получение объекта Player по сообщению
let th = this;
return new Promise((res, rej)=>{
let ponyBox = qs('pony-box');
if(!ponyBox || qs('.pony-box-name-text', ponyBox).innerText != msg.author) qs('.chat-line-name-content', msg.elem).click();
setTimeout(()=>{
res(th.get());
}, 100);
});
}
},
onGameLoad(f){
this.gameLoadedListeners.push(f);
},
wshook: {
enabled: localStorage.disableWsHook != 'true',
send: [],
receive: [],
attach(type, func){
return this[type].push(func) - 1;
},
detach(type, index){
this[type].splice(index, 1);
},
enable(){
localStorage.disableWsHook = 'false';
location.reload();
},
disable(){
localStorage.disableWsHook = 'true';
location.reload();
},
getSocket(){
return ws;
}
},
menuButton: {
list: [],
add(text, func){
return this.list.push({ text, func }) - 1;
},
remove(index){
this.list.splice(index, 1);
}
},
graphics: {
getGlContext(){
return gl;
},
readPixel(x, y){
let px = new Uint8Array(4);
gl.readPixels(devicePixelRatio*x, devicePixelRatio*(gl.canvas.clientHeight-y), 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, px);
return px;
},
readAllPixels(){
let pixels = new Uint8Array(gl.drawingBufferWidth * gl.drawingBufferHeight * 4);
gl.readPixels(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight, gl.RGBA, gl.UNSIGNED_BYTE, pixels);
return pixels;
}
},
tweaker: {
optionsUI: tweakerUI,
scriptsUI: scriptsUI,
addScriptByURL: addScriptByURL,
antiAfk: antiAfk
},
gameLoadedListeners: []
}
//[/API]
if(localStorage.ptToWindow) w.pt = pt;
function getFormattedDateTime(dd, td, md){
let d = new Date();
let arr = [
(d.getDate()).toString(),
(d.getMonth()+1).toString(),
d.getFullYear().toString(),
d.getHours().toString(),
d.getMinutes().toString(),
d.getSeconds().toString()
];
arr.forEach((e,i,o)=>{
o[i] = (e.length>1?e:'0'+e);
});
let date = arr.slice(0, 3), time = arr.slice(3);
return `${date.join(dd)}${md}${time.join(td)}`;
}
function getFormattedDate(){
let d = new Date();
let arr = [
(d.getDate()).toString(),
(d.getMonth()+1).toString(),
d.getFullYear().toString()
];
arr.forEach((e,i,o)=>{
o[i] = (e.length>1?e:'0'+e);
});
return `${arr.join('.')}`;
}
function getFormattedTime(){
let d = new Date();
let arr = [
d.getHours().toString(),
d.getMinutes().toString()
];
arr.forEach((e,i,o)=>{
o[i] = (e.length>1?e:'0'+e);
});
return `${arr.join(':')}`;
}
function rgbToHex(array, hash){
let str = hash?'#':'';
for(let i = 0; i < 3; i++){
if(array[i] < 16) str += '0';
str += array[i].toString(16);
}
return str;
}
let aaIid = 0;
function antiAfk(){
if(aaIid){
clearInterval(aaIid);
return;
}
aaIid = setInterval(()=>{
if(pt.status.get() == 'away'){
pt.action('turn head');
}
}, 200);
}
function getDBNames(cb){
indexedDB.databases().then(db=>cb(db.map(e=>e.name)));
}
function getObjectStoreNames(db, cb){
indexedDB.open(db).onsuccess = d=>cb(d.target.result.objectStoreNames);
}
function getDataFromDB(db, st, cb){
try{
let open = indexedDB.open(db);
open.onsuccess = ()=>{
let db = open.result;
let tr = db.transaction(st, 'readonly');
try{
let storage = tr.objectStore(st);
let req = storage.getAll();
req.onsuccess = ()=>{
let obj = [];
for(let i in req.result){
let e = req.result[i];
obj.push(e);
}
if(cb) cb(obj);
}
req.onerror = ()=>{
if(cb) cb({}, req.error);
}
}catch(e){
if(cb) cb({}, e);
}
}
open.onerror = ()=>{
if(cb) cb({}, open.error);
}
}catch(e){
if(cb) cb({}, e);
}
}
function putDataToDB(db, st, ind, data, cb){
try{
let open = indexedDB.open(db);
open.onsuccess = ()=>{
let db = open.result;
let tr = db.transaction(st, 'readwrite');
try{
let storage = tr.objectStore(st);
let req = storage.put(data, ind);
req.onsuccess = ()=>{
if(cb) cb(null);
}
req.onerror = ()=>{
if(cb) cb({}, req.error);
}
}catch(e){
if(cb) cb({}, e);
}
}
open.onerror = ()=>{
if(cb) cb({}, open.error);
}
}catch(e){
if(cb) cb({}, e);
}
}
function deleteDataFromDB(db, st, ind, cb){
try{
let open = indexedDB.open(db);
open.onsuccess = ()=>{
let db = open.result;
let tr = db.transaction(st, 'readwrite');
try{
let storage = tr.objectStore(st);
let req = storage.delete(ind);
req.onsuccess = ()=>{
if(cb) cb(null);
}
req.onerror = ()=>{
if(cb) cb({}, req.error);
}
}catch(e){
if(cb) cb({}, e);
}
}
open.onerror = ()=>{
if(cb) cb({}, open.error);
}
}catch(e){
if(cb) cb({}, e);
}
}
function convertToPony(jsonStr){
let jp = JSON.parse(jsonStr);
let obj = {};
obj.id = jp.id;
obj.time = new Date(jp.time);
obj.data = new Uint8Array(Object.values(jp.data));
return obj;
}
function wshook(cb){
cb = cb??console.log;
let property = Object.getOwnPropertyDescriptor(MessageEvent.prototype, 'data');
let data = property.get;
function msgHandler() {
if (!(this.currentTarget instanceof WebSocket)) return data.call(this);
let msg = data.call(this);
Object.defineProperty(this, 'data', { value: msg });
return cb({ data: msg, socket: this.currentTarget, event: this }) || msg;
}
property.get = msgHandler;
Object.defineProperty(MessageEvent.prototype, 'data', property);
}
function saveChat(){
let el = document.querySelector('.chat-log-scroll-inner');
if(!el) return;
let text = el.innerText.replaceAll('[', ' [');
let l = document.createElement('a');
l.href = URL.createObjectURL(new Blob([text]));
l.download = `chatLog_${getFormattedDateTime('.', ':', '-')}.txt`
l.click();
URL.revokeObjectURL(l.href);
}
function box(obj){
if(typeof obj == 'string') obj = {text: obj};
let mh = ce('div');
mh.style.merge({
position: 'fixed',
left: '0',
top: '0',
width: '100%',
height: '100%',
zIndex: '999999',
overflow: 'hidden',
overflowY: 'auto',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
cursor: 'default',
userSelect: 'none',
fontSize: '120%'
});
let wrapper = ce('div');
wrapper.style.display = 'block';
let header = ce('div');
header.innerText = obj.header??'';
header.style.merge({
width: '100%',
padding: '0.7em',
borderBottom: 'solid 2px white'
});
let closeBtn = ce('span');
closeBtn.innerText = '\u2573';
closeBtn.style.merge({
paddingLeft: '0.5em',
cursor: 'pointer',
float: 'right'
});
closeBtn.addEventListener('click', ()=>{
mh.parentNode.removeChild(mh);
if(obj.onclose) obj.onclose();
});
let abox = ce('div');
abox.style.merge({
background: '#212121',
color: 'white',
border: 'solid 2px white',
borderRadius: '3px',
width: obj.fixedSize?((obj.width??600)+'px'):'',
height: obj.fixedSize?((obj.height??400)+'px'):''
});
let text = ce('div');
text.style.padding = '0.7em';
if(obj.text) text.innerHTML = obj.text;
else if(obj.elem) text.appendChild(obj.elem);
else throw new Error('Необходимо задать свойство text или elem объекта.');
header.appendChild(closeBtn);
abox.appendChild(header);
abox.appendChild(text);
wrapper.appendChild(abox);
mh.appendChild(wrapper);
document.body.appendChild(mh);
return {
input: obj,
box: mh,
close: (f)=>{ mh.parentNode.removeChild(mh); if(obj.onclose && !f) obj.onclose(); }
};
}
function tweakerUI(){
let d = ce('div'), cont = ce('div'), abox;
cont.style.maxHeight = '20em';
cont.style.overflow = 'auto';
for(let i in twOptions){
let e = twOptions[i];
let el = ce('div');
el.style.merge({padding: '5px', margin: '5px', borderRadius: '5px', background: '#171717', cursor: 'pointer'});
el.innerText = e.name;
// eslint-disable-next-line no-loop-func
el.addEventListener('click', ()=>{
let elem = ce('div'), des = ce('div'), inp = ce('input'), btn = ce('button'), bx;
des.innerText = e.description??'';
inp.type = e.type=='bool'?'checkbox':e.type;
if(e.type == 'bool') inp.checked = e.value;
else inp.value = e.value;
inp.style.display = 'block';
inp.style.margin = '1em auto';
btn.innerText = 'OK';
btn.className = 'btn btn-default';
btn.style.display = 'block';
btn.style.padding = '0.3em 2em';
btn.style.margin = '0 auto';
btn.addEventListener('click', ()=>{
twOptions[i].value = e.type=='bool'?inp.checked:inp.value;
bx.close();
});
elem.appendChild(des);
elem.appendChild(inp);
elem.appendChild(btn);
bx = box({
header: e.name,
elem: elem
});
});
cont.appendChild(el);
}
d.appendChild(cont);
let btn = ce('button');
btn.className = 'btn btn-default';
btn.style.display = 'block';
btn.style.padding = '0.3em 2em';
btn.style.margin = '1em auto';
btn.innerText = 'Сохранить';
btn.addEventListener('click', ()=>{
localStorage.twOptions = JSON.stringify(twOptions);
setTimeout(()=>location.reload(), 200);
});
d.appendChild(btn);
abox = box({header: 'Настройки PTTW', elem: d});
}
function runScript(s){
try{
if(!s.lang || s.lang == 'js'){
return w.eval(decodeURIComponent(atob(s.code)));
}else if(s.lang == 'python'){
if(!w.pyinit){
fetch('https://nekit270.ch/get.php?f=/files/brython.js').then(f=>{
f.text().then(t=>{
w.eval(t);
w.brython();
w.__BRYTHON__.imported.exec = {};
w.__BRYTHON__.imported.pt = pt;
w.eval(w.__BRYTHON__.py2js(decodeURIComponent(atob(s.code))).to_js());
w.pyinit = true;
});
});
}else{
w.eval(w.__BRYTHON__.py2js(decodeURIComponent(atob(s.code))).to_js());
}
}
}catch(ex){
box({
header: 'Ошибка',
text: `В скрипте "${s.name}" произошла ошибка. Работа скрипта остановлена.
Информация об ошибке
Возможные варианты решения проблемы:
| Сообщение | |
| Кол-во повторов | |
| Задержка (в мс) |