2Pies Port - auto port dashboard with Discord alerts and 8-city controls
// ==UserScript==
// @name 2Pies Port
// @namespace 2pies.port
// @description 2Pies Port - auto port dashboard with Discord alerts and 8-city controls
// @license MIT
// @icon https://rycamelot1-a.akamaihd.net/fb/e2/src/img/items/70/911.jpg
// @include *.facebook.com/kingdomsofcamelot*
// @include *.rycamelot.com/*main_src.php*
// @include *.kingdomsofcamelot.com/*main_src.php*
// @include *.wonderhill.com/*main_src.php*
// @connect *
// @connect greasyfork.org
// @grant unsafeWindow
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_xmlhttpRequest
// @grant GM_log
// @version 3.24
// @run-at document-idle
// ==/UserScript==
/* jshint esversion: 11, browser: true, laxbreak: true, sub: true, -W097 */
/* globals unsafeWindow, GM, GM_getValue, GM_setValue, GM_xmlhttpRequest, GM_log, ActiveXObject */
const JSON2 = JSON;
let PortCoremainPop;
const Tabs = {};
const Cities = {};
let DEBUG_TRACE = false;
const dlgHeight = 700;
const dlgWidth = 860;
const Version = "3.24 License Fix";
const DISCORD_WEBHOOK_VIDEO_URL = "https://www.youtube.com/watch?v=fKksxz2Gdnc";
const AutoPort = {
portSarcasm: false,
portSarcasmContent: "BAH BYE!",
portWarning: false,
portWarningContent: "I'm Being Attacked/Scouted, Attempting To Autoport!",
portMessage: false,
portRefuge: true,
portOrder: false,
portThresholdAttack: 10000,
portThresholdScout: 10,
portRecheck: 10,
portCitiesAttack: {0:false,1:false,2:false,3:false,4:false,5:false,6:false,7:false},
portCitiesScout: {0:false,1:false,2:false,3:false,4:false,5:false,6:false,7:false},
discordWebhook: "",
discordEnabled: false,
discordAttack: true,
discordScout: true,
discordSuccess: true,
discordFail: true,
discordMention: "",
updateUrl: ""
};
function getCitySlotCount(){
var n = (Cities && Cities.numCities) ? parseInt(Cities.numCities, 10) : 0;
if (isNaN(n) || n < 0) n = 0;
return n < 8 ? 8 : n;
}
function ensureCitySettings(){
if (!AutoPort.portCitiesAttack) AutoPort.portCitiesAttack = {};
if (!AutoPort.portCitiesScout) AutoPort.portCitiesScout = {};
var maxCities = getCitySlotCount();
for (var i = 0; i < maxCities; i++) {
if (AutoPort.portCitiesAttack[i] == null) AutoPort.portCitiesAttack[i] = false;
if (AutoPort.portCitiesScout[i] == null) AutoPort.portCitiesScout[i] = false;
}
}
function setCities(){
Cities.numCities = 0;
Cities.cities = [];
Cities.byID = {};
Cities.byKey = {}; // fast lookup by cityId:tileId for incoming attacks/scouts
Cities.byCoords = {}; // fallback lookup by x,y if incoming march has destination coords
try {
if (!unsafeWindow.seed || !unsafeWindow.seed.cities || !unsafeWindow.seed.cities.length) {
ensureCitySettings();
return;
}
Cities.numCities = unsafeWindow.seed.cities.length;
for (var i=0; i < Cities.numCities; i++){
var rawCity = unsafeWindow.seed.cities[i];
if (!rawCity) continue;
var city = {};
city.idx = i;
city.id = parseInt(rawCity[0], 10);
city.name = rawCity[1] || ('City ' + (i + 1));
city.x = parseInt(rawCity[2], 10);
city.y = parseInt(rawCity[3], 10);
city.tileId = parseInt(rawCity[5], 10);
city.provId = parseInt(rawCity[4], 10);
Cities.cities[i] = city;
Cities.byID[String(city.id)] = city;
Cities.byKey[String(city.id) + ':' + String(city.tileId)] = city;
if (!isNaN(city.x) && !isNaN(city.y)) Cities.byCoords[String(city.x) + ':' + String(city.y)] = city;
}
} catch (e) {
if (DEBUG_TRACE) logit('setCities error: ' + e);
}
ensureCitySettings();
}
function refreshCities(){
try { setCities(); } catch (e) { if (DEBUG_TRACE) logit('refreshCities error: ' + e); }
}
function hasOwn(obj, key) {
return Object.prototype.hasOwnProperty.call(obj, key);
}
function PortCoreHttpRequest(options) {
if (typeof GM_xmlhttpRequest === 'function') {
return GM_xmlhttpRequest(options);
}
if (typeof GM !== 'undefined' && GM && typeof GM.xmlHttpRequest === 'function') {
return GM.xmlHttpRequest(options);
}
throw new Error('No userscript HTTP request API available');
}
function PortCoreLog(message) {
if (typeof GM_log === 'function') {
GM_log(message);
} else if (window.console && console.log) {
console.log(message);
}
}
function PortCoreGetValue(key, fallback) {
try {
if (typeof GM_getValue === 'function') return GM_getValue(key, fallback);
} catch (e) {}
return fallback;
}
function PortCoreSetValue(key, value) {
try {
if (typeof GM_setValue === 'function') GM_setValue(key, value);
} catch (e) {
if (DEBUG_TRACE) PortCoreLog('GM_setValue failed: ' + e);
}
}
setCities();
const PortCoreWinManager = {
wins : {}, // prefix : PortCoreCPopup obj
get : function (prefix){
var t = PortCoreWinManager;
return t.wins[prefix];
},
add : function (prefix, pop){
var t = PortCoreWinManager;
t.wins[prefix] = pop;
if (unsafeWindow.cpopupWins == null)
unsafeWindow.cpopupWins = {};
unsafeWindow.cpopupWins[prefix] = pop;
},
delete : function (prefix){
var t = PortCoreWinManager;
delete t.wins[prefix];
delete unsafeWindow.cpopupWins[prefix];
}
}
// creates a 'popup' div
// prefix must be a unique (short) name for the popup window
function PortCoreCPopup(prefix, x, y, width, height, enableDrag, onClose) {
var pop = PortCoreWinManager.get(prefix);
if (pop) {
pop.show(false);
return pop;
}
this.BASE_ZINDEX = 111111;
// protos ...
this.show = show;
this.toggleHide = toggleHide;
this.getTopDiv = getTopDiv;
this.getMainDiv = getMainDiv;
this.getLayer = getLayer;
this.setLayer = setLayer;
this.setEnableDrag = setEnableDrag;
this.getLocation = getLocation;
this.setLocation = setLocation;
this.focusMe = focusMe;
this.unfocusMe = unfocusMe;
this.centerMe = centerMe;
this.destroy = destroy;
// object vars ...
this.div = document.createElement('div');
this.prefix = prefix;
this.onClose = onClose;
var t = this;
this.div.className = 'PortCoreCPopup ' + prefix + '_CPopup';
this.div.id = prefix + '_outer';
this.div.style.background = "#fff";
this.div.style.zIndex = this.BASE_ZINDEX; // KOC modal is 100210 ?
this.div.style.display = 'none';
this.div.style.width = width + 'px';
this.div.style.height = height + 'px';
this.div.style.position = "absolute";
this.div.style.top = y + 'px';
this.div.style.left = x + 'px';
var topClass = 'CPopupTop ' + prefix + '_CPopupTop';
var m = '<TABLE cellspacing=0 width=100% height=100%><TR id="' + prefix + '_bar" class="' + topClass + '"><TD style="-moz-border-radius-topleft: 20px; border-top-left-radius: 20px;" width=99%><SPAN id="'+ prefix +'_top"></span></td>\
<TD id=' + prefix + '_X align=right valign=middle onmouseover="this.style.cursor=\'pointer\'" style="color:#fff; background:#333; font-weight:bold; font-size:14px; padding:0px 5px; -moz-border-radius-topright: 20px; border-top-right-radius: 20px;">X</td></tr>\
<TR><TD height=100% valign=top class="CPopMain ' + prefix + '_CPopMain" colspan=2 id="' + prefix + '_main"></td></tr></table>';
document.body.appendChild(this.div);
this.div.innerHTML = m;
document.getElementById(prefix + '_X').addEventListener('click', e_XClose, false);
this.dragger = new PortCoreCWinDrag(document.getElementById(prefix + '_bar'), this.div, enableDrag);
this.div.addEventListener('mousedown', e_divClicked, false);
PortCoreWinManager.add(prefix, this);
function e_divClicked() {
t.focusMe();
}
function e_XClose() {
t.show(false);
if (t.onClose != null)
t.onClose();
}
function focusMe() {
t.setLayer(5);
for (var k in unsafeWindow.cpopupWins) {
if (k != t.prefix)
unsafeWindow.cpopupWins[k].unfocusMe();
}
}
function unfocusMe() {
t.setLayer(-5);
}
function getLocation() {
return {
x: parseInt(this.div.style.left, 10),
y: parseInt(this.div.style.top, 10)
};
}
function setLocation(loc) {
t.div.style.left = loc.x + 'px';
t.div.style.top = loc.y + 'px';
}
function destroy() {
document.body.removeChild(t.div);
PortCoreWinManager.delete(t.prefix);
}
function centerMe(parent) {
var coords = (parent == null) ? getClientCoords(document.body) : getClientCoords(parent);
var x = ((coords.width - parseInt(t.div.style.width, 10)) / 2) + coords.x;
var y = ((coords.height - parseInt(t.div.style.height, 10)) / 2) + coords.y;
if (x < 0)
x = 0;
if (y < 0)
y = 0;
t.div.style.left = x + 'px';
t.div.style.top = y + 'px';
}
function setEnableDrag(tf) {
t.dragger.setEnable(tf);
}
function setLayer(zi) {
t.div.style.zIndex = '' + (this.BASE_ZINDEX + zi);
}
function getLayer() {
return parseInt(t.div.style.zIndex, 10) - this.BASE_ZINDEX;
}
function getTopDiv() {
return document.getElementById(this.prefix + '_top');
}
function getMainDiv() {
return document.getElementById(this.prefix + '_main');
}
function show(tf) {
if (tf) {
t.div.style.display = 'block';
t.focusMe();
} else {
t.div.style.display = 'none';
}
return tf;
}
function toggleHide(t) {
if (t.div.style.display == 'block') {
return t.show(false);
} else {
return t.show(true);
}
}
}
function PortCoreCWinDrag (clickableElement, movingDiv, enabled) {
var t=this;
this.setEnable = setEnable;
this.setBoundRect = setBoundRect;
this.lastX = null;
this.lastY = null;
this.enabled = true;
this.moving = false;
this.theDiv = movingDiv;
this.body = document.body;
this.ce = clickableElement;
this.moveHandler = new CeventMove(this).handler;
this.outHandler = new CeventOut(this).handler;
this.upHandler = new CeventUp(this).handler;
this.downHandler = new CeventDown(this).handler;
this.clickableRect = null;
this.boundRect = null;
this.bounds = null;
this.enabled = false;
if (enabled == null)
enabled = true;
this.setEnable (enabled);
function setBoundRect (b){ // this rect (client coords) will not go outside of current body
this.boundRect = b;
this.bounds = null;
}
function setEnable (enable){
if (enable == t.enabled)
return;
if (enable){
clickableElement.addEventListener('mousedown', t.downHandler, false);
t.body.addEventListener('mouseup', t.upHandler, false);
} else {
clickableElement.removeEventListener('mousedown', t.downHandler, false);
t.body.removeEventListener('mouseup', t.upHandler, false);
}
t.enabled = enable;
}
function CeventDown (that){
this.handler = handler;
var t = that;
function handler (me){
if (t.bounds == null){
t.clickableRect = getClientCoords(clickableElement);
t.bodyRect = getClientCoords(document.body);
if (t.boundRect == null)
t.boundRect = t.clickableRect;
t.bounds = {top:10-t.clickableRect.height, bot:t.bodyRect.height-25, left:40-t.clickableRect.width, right:t.bodyRect.width-25};
}
if (me.button==0 && t.enabled){
t.body.addEventListener('mousemove', t.moveHandler, true);
t.body.addEventListener('mouseout', t.outHandler, true);
t.lastX = me.clientX;
t.lastY = me.clientY;
t.moving = true;
}
}
}
function CeventUp (that){
this.handler = handler;
var t = that;
function handler (me){
if (me.button==0 && t.moving)
_doneMoving(t);
}
}
function _doneMoving (t){
t.body.removeEventListener('mousemove', t.moveHandler, true);
t.body.removeEventListener('mouseout', t.outHandler, true);
t.moving = false;
}
function CeventOut (that){
this.handler = handler;
var t = that;
function handler (me){
if (me.button==0){
t.moveHandler (me);
}
}
}
function CeventMove (that){
this.handler = handler;
var t = that;
function handler (me){
if (t.enabled && !t.wentOut){
var newTop = parseInt(t.theDiv.style.top) + me.clientY - t.lastY;
var newLeft = parseInt(t.theDiv.style.left) + me.clientX - t.lastX;
if (newTop < t.bounds.top){ // if out-of-bounds...
newTop = t.bounds.top;
_doneMoving(t);
} else if (newLeft < t.bounds.left){
newLeft = t.bounds.left;
_doneMoving(t);
} else if (newLeft > t.bounds.right){
newLeft = t.bounds.right;
_doneMoving(t);
} else if (newTop > t.bounds.bot){
newTop = t.bounds.bot;
_doneMoving(t);
}
t.theDiv.style.top = newTop + 'px';
t.theDiv.style.left = newLeft + 'px';
t.lastX = me.clientX;
t.lastY = me.clientY;
}
}
}
}
function getClientCoords(e) {
if (e == null)
return {
x: null,
y: null,
width: null,
height: null
};
var ret = {
x: 0,
y: 0,
width: e.clientWidth,
height: e.clientHeight
};
while (e.offsetParent != null) {
ret.x += e.offsetLeft;
ret.y += e.offsetTop;
e = e.offsetParent;
}
return ret;
}
const PortCoretabManager = {
tabList : {}, // {name, obj, div}
currentTab : null,
init : function (mainDiv){
var t = PortCoretabManager;
var sorter = [];
for (var k in Tabs){
if (!Tabs[k].tabDisabled){
t.tabList[k] = {};
t.tabList[k].name = k;
t.tabList[k].tabColor = Tabs[k].tabColor?Tabs[k].tabColor:'blue';
t.tabList[k].obj = Tabs[k];
if (Tabs[k].tabLabel != null)
t.tabList[k].label = Tabs[k].tabLabel;
else
t.tabList[k].label = k;
if (Tabs[k].tabOrder != null)
sorter.push([Tabs[k].tabOrder, t.tabList[k]]);
else
sorter.push([1000, t.tabList[k]]);
t.tabList[k].div = document.createElement('div');
}
}
sorter.sort (function (a,b){return a[0]-b[0]});
var m = '';
m += '<div class="portCoreTopShell">';
m += ' <span class="portCoreTopBrand">2Pies Port</span>';
if (sorter.length > 1) {
m += '<span class="portCoreTopTabs">';
for (var i=0; i<sorter.length; i++) {
m += '<span id="nttc'+ sorter[i][1].name +'" class="portCoreTopTab">'+ sorter[i][1].label +'</span>';
}
m += '</span>';
}
m += '</div>';
PortCoremainPop.getTopDiv().innerHTML = m;
var contentDiv = document.createElement('div');
contentDiv.id = 'PortCoreMain_content';
mainDiv.appendChild(contentDiv);
for (var k in t.tabList) {
var tabButton = document.getElementById('nttc'+ k);
if (tabButton) tabButton.addEventListener('click', this.e_clickedTab, false);
var div = t.tabList[k].div;
div.style.display = 'none';
div.style.height = '100%';
contentDiv.appendChild(div);
try {
t.tabList[k].obj.init(div);
} catch (e){
div.innerHTML = "INIT ERROR: "+ e;
}
}
if (t.currentTab == null)
t.currentTab = sorter[0][1];
t.currentTab.div.style.display = 'block';
},
hideTab : function (){
var t = PortCoretabManager;
t.currentTab.obj.hide();
},
showTab : function (){
var t = PortCoretabManager;
t.currentTab.obj.show();
},
setTabStyle : function (Tab, selected){
var e = document.getElementById('nttc'+ Tab.name);
if (!e) return;
e.className = selected ? 'portCoreTopTab active' : 'portCoreTopTab';
},
e_clickedTab : function (e){
var t = PortCoretabManager;
PortCoremainPop.show (true);
if (e.target.id)
var newTab = t.tabList[e.target.id.substring(4)];
else
var newTab = t.tabList[e.target.parentNode.id.substring(4)];
if (t.currentTab.name != newTab.name){
t.setTabStyle (t.currentTab, false);
t.setTabStyle (newTab, true);
t.currentTab.obj.hide ();
t.currentTab.div.style.display = 'none';
t.currentTab = newTab;
newTab.div.style.display = 'block';
}
newTab.obj.show();
},
}
function eventHideShow() {
if (PortCoremainPop.toggleHide(PortCoremainPop)) {
PortCoretabManager.showTab();
} else {
PortCoretabManager.hideTab();
}
}
function AddMainTabLink(text, eventListener, mouseListener) {
var a = document.createElement('a');
a.className = 'button20';
a.innerHTML = '<span style="color: #ff6">' + text + '</span>';
a.className = 'tab';
var tabs = document.getElementById('main_engagement_tabs');
if (!tabs) {
tabs = document.getElementById('topnav_msg');
if (tabs)
tabs = tabs.parentNode;
}
if (tabs) {
var e = tabs.parentNode;
var gmTabs = null;
for (var i = 0; i < e.childNodes.length; i++) {
var ee = e.childNodes[i];
//if (ee.tagName=='DIV') logit ("CHILD: "+ ee.tagName +' : '+ ee.className+' : '+ ee.id);
if (ee.tagName && ee.tagName == 'DIV' && ee.className == 'tabs_engagement' && ee.id != 'main_engagement_tabs') {
gmTabs = ee;
break;
}
}
if (gmTabs == null) {
gmTabs = document.createElement('div');
gmTabs.className = 'tabs_engagement';
tabs.parentNode.insertBefore(gmTabs, tabs);
gmTabs.style.whiteSpace = 'normal';
gmTabs.style.width = '735px';
}
gmTabs.style.height = '0%';
gmTabs.style.overflow = 'auto';
if (gmTabs.firstChild)
gmTabs.insertBefore(a, gmTabs.firstChild);
else
gmTabs.appendChild(a);
a.addEventListener('click', eventListener, false);
if (mouseListener != null)
a.addEventListener('mousedown', mouseListener, true);
return a;
}
return null;
}
function sendChat (msg){
document.getElementById("mod_comm_input").value = msg;
unsafeWindow.Chat.sendChat ();
}
function sendComposedMail (sendTo, subject, msg) {
var params = unsafeWindow.Object.clone(unsafeWindow.g_ajaxparams);
params.emailTo = sendTo;
params.subject = subject;
params.message = msg;
params.requestType = "COMPOSED_MAIL";
new AjaxRequest(unsafeWindow.g_ajaxpath + "ajax/getEmail.php" + unsafeWindow.g_ajaxsuffix, {
method:"post",
parameters:params,
onSuccess:function(message){
},
onFailure:function(){
}
})
}
function sendDiscordWebhook(content, embedObj) {
try {
if (!AutoPort.discordEnabled) return;
if (!AutoPort.discordWebhook || AutoPort.discordWebhook === "") return;
var payload = {};
var msg = "";
if (AutoPort.discordMention && AutoPort.discordMention !== "") {
msg += AutoPort.discordMention + " ";
}
if (content) msg += content;
payload.content = msg;
if (embedObj) payload.embeds = [embedObj];
PortCoreHttpRequest({
method: "POST",
url: AutoPort.discordWebhook,
headers: {
"Content-Type": "application/json"
},
data: JSON.stringify(payload),
onload: function (res) {
if (DEBUG_TRACE) logit("Discord webhook sent: " + res.status);
},
onerror: function (err) {
if (DEBUG_TRACE) logit("Discord webhook failed: " + inspect(err, 2, 1));
}
});
} catch (e) {
if (DEBUG_TRACE) logit("sendDiscordWebhook error: " + e);
}
}
function buildDiscordEmbed(title, description, color) {
return {
title: title,
description: description,
color: color || 16711680,
footer: {
text: "2Pies Port v" + Version
},
timestamp: new Date().toISOString()
};
}
function testDiscordWebhook() {
try {
AutoPort.discordEnabled = true;
SaveAutoPortData();
var statusNode = document.getElementById('discordTestStatus');
if (statusNode) statusNode.innerHTML = 'Sending test...';
if (!AutoPort.discordWebhook || AutoPort.discordWebhook === '') {
if (statusNode) statusNode.innerHTML = 'Enter webhook URL first';
return;
}
var payload = {};
var msg = '🧪 Discord webhook test from 2Pies Port';
if (AutoPort.discordMention && AutoPort.discordMention !== '') {
msg = AutoPort.discordMention + ' ' + msg;
}
payload.content = msg;
payload.embeds = [buildDiscordEmbed(
'Webhook Test',
'This is a manual test from the 2Pies Port tab. If you can see this, webhook alerts are working.',
3447003
)];
PortCoreHttpRequest({
method: 'POST',
url: AutoPort.discordWebhook,
headers: {
'Content-Type': 'application/json'
},
data: JSON.stringify(payload),
onload: function (res) {
if (statusNode) {
statusNode.innerHTML = (res.status >= 200 && res.status < 300) ? 'Test sent OK' : ('Discord error ' + res.status);
}
if (DEBUG_TRACE) logit('Discord webhook test status: ' + res.status + ' ' + res.responseText);
},
onerror: function (err) {
if (statusNode) statusNode.innerHTML = 'Webhook test failed';
if (DEBUG_TRACE) logit('Discord webhook test failed: ' + inspect(err, 2, 1));
}
});
} catch (e) {
var statusNode = document.getElementById('discordTestStatus');
if (statusNode) statusNode.innerHTML = 'Webhook test error';
if (DEBUG_TRACE) logit('testDiscordWebhook error: ' + e);
}
}
function inspect(obj, maxLevels, level, doFunctions){
var str = '', type, msg;
if(level == null) level = 0;
if(maxLevels == null) maxLevels = 1;
if(maxLevels < 1)
return 'Inspect Error: Levels number must be > 0';
if(obj == null)
return 'ERROR: Object is NULL\n';
var indent = '';
for (var i=0; i<level; i++)
indent += ' ';
for (var property in obj) {
try {
type = matTypeof(obj[property]);
if (doFunctions==true && (type == 'function')){
str += indent + '(' + type + ') ' + property + "[FUNCTION]\n";
} else if (type != 'function') {
str += indent + '(' + type + ') ' + property + ( (obj[property]==null)?(': null'):('')) +' = '+ obj[property] +"\n";
}
if((type=='object' || type=='array') && (obj[property] != null) && (level+1 < maxLevels))
str += inspect(obj[property], maxLevels, level+1, doFunctions); // recurse
}
catch(err) {
// Is there some properties in obj we can't access? Print it red.
if(typeof(err) == 'string') msg = err;
else if(err.message) msg = err.message;
else if(err.description) msg = err.description;
else msg = 'Unknown';
str += '(Error) ' + property + ': ' + msg +"\n";
}
}
str += "\n";
return str;
}
function logit (msg) {
var now = new Date();
PortCoreLog(getServerId() + ' @ ' + now.toTimeString().substring(0, 8) + '.' + now.getMilliseconds() + ': ' + msg);
}
function RefreshCamelot() { //This piece of code was taken from KoC Power BOT. All credit goes to them for this piece. Thanks guys/gals!
var serverId = getServerId();
var goto = window.location.protocol + '//apps.facebook.com/kingdomsofcamelot/?s=' + serverId;
if (document.URL.match(/standalone=1/i)) {
goto = window.location.protocol + '//www.wonderhill.com/games/kingdoms-of-camelot/play?s=' + serverId;
};
setTimeout (function (){window.top.location = goto;}, 0);
}
let PortCoreLoopTimer = null;
let PortCoreLoopRunning = false;
let PortCoreNextScanAt = 0;
const PortCoreDetection = {
lastScanText: 'Never',
sourceText: 'Not scanned yet',
incomingCount: 0,
attackCount: 0,
scoutCount: 0,
matchedCount: 0,
skippedCount: 0,
lastThreatText: 'None',
lastErrorText: ''
};
function getPortRecheckSeconds() {
var seconds = parseInt(AutoPort.portRecheck, 10);
if (isNaN(seconds) || seconds < 2) seconds = 2;
if (seconds > 300) seconds = 300;
AutoPort.portRecheck = seconds;
return seconds;
}
function startAutoPortLoop() {
if (PortCoreLoopRunning) return;
PortCoreLoopRunning = true;
scheduleNextAutoPortScan(300);
}
function restartAutoPortLoop() {
if (PortCoreLoopTimer) clearTimeout(PortCoreLoopTimer);
PortCoreLoopRunning = true;
scheduleNextAutoPortScan(300);
}
function scheduleNextAutoPortScan(delayMs) {
if (PortCoreLoopTimer) clearTimeout(PortCoreLoopTimer);
if (delayMs == null) delayMs = getPortRecheckSeconds() * 1000;
PortCoreNextScanAt = Date.now() + delayMs;
PortCoreLoopTimer = setTimeout(function () {
try {
checkAutoPort();
} catch (e) {
if (DEBUG_TRACE) logit('checkAutoPort error: ' + e);
}
scheduleNextAutoPortScan();
}, delayMs);
}
function formatPortCoreTime(d) {
try {
return d.toLocaleTimeString();
} catch (e) {
return String(d);
}
}
function setPortCoreDetectionStatus(fields) {
for (var key in fields) {
if (hasOwn(fields, key)) PortCoreDetection[key] = fields[key];
}
updatePortCoreDetectionUi();
}
function updatePortCoreDetectionUi() {
function setHtml(id, value) {
var el = document.getElementById(id);
if (el) el.innerHTML = String(value == null ? '' : value);
}
setHtml('portDetectLastScan', PortCoreDetection.lastScanText);
setHtml('portDetectSource', PortCoreDetection.sourceText);
setHtml('portDetectIncoming', PortCoreDetection.incomingCount + ' total / ' + PortCoreDetection.attackCount + ' attack / ' + PortCoreDetection.scoutCount + ' scout');
setHtml('portDetectMatched', PortCoreDetection.matchedCount + ' matched / ' + PortCoreDetection.skippedCount + ' skipped');
setHtml('portDetectThreat', PortCoreDetection.lastThreatText);
setHtml('portDetectError', PortCoreDetection.lastErrorText || 'OK');
}
function looksLikeIncomingMarch(obj) {
if (!obj || typeof obj !== 'object') return false;
var typeValue = obj.marchType != null ? obj.marchType : (obj.mt != null ? obj.mt : obj.type);
var hasType = typeValue != null || obj.attackType != null || obj.scoutType != null;
var hasTarget = obj.toCityId != null || obj.cityId != null || obj.cid != null || obj.toTileId != null || obj.toXCoord != null || obj.xCoord != null;
return hasType && hasTarget;
}
function looksLikeIncomingQueue(obj) {
if (!obj || typeof obj !== 'object') return false;
if (looksLikeIncomingMarch(obj)) return true;
var checked = 0;
for (var key in obj) {
if (!hasOwn(obj, key)) continue;
checked++;
if (looksLikeIncomingMarch(obj[key])) return true;
if (checked > 25) break;
}
return false;
}
function addIncomingSource(list, seen, name, queue) {
if (!queue || seen[name]) return;
if (!looksLikeIncomingQueue(queue)) return;
seen[name] = true;
list.push({ name: name, queue: queue });
}
function getIncomingQueueSources() {
var sources = [];
var seen = {};
var seed = (unsafeWindow && unsafeWindow.seed) ? unsafeWindow.seed : null;
if (seed) {
addIncomingSource(sources, seen, 'seed.queue_atkinc', seed.queue_atkinc);
addIncomingSource(sources, seen, 'seed.queue_inc', seed.queue_inc);
addIncomingSource(sources, seen, 'seed.incoming', seed.incoming);
addIncomingSource(sources, seen, 'seed.queueIncoming', seed.queueIncoming);
// Controlled fallback: scan seed only, not the whole unsafeWindow, so it stays fast.
for (var key in seed) {
if (!hasOwn(seed, key)) continue;
if (seen['seed.' + key]) continue;
if (/atk|attack|scout|incoming|inc/i.test(key)) {
addIncomingSource(sources, seen, 'seed.' + key, seed[key]);
}
}
}
return sources;
}
function collectIncomingItems(queue, sourceName, out) {
if (!queue) return;
if (looksLikeIncomingMarch(queue)) {
out.push({ source: sourceName, march: queue });
return;
}
if (Array.isArray(queue)) {
for (var i = 0; i < queue.length; i++) {
if (looksLikeIncomingMarch(queue[i])) out.push({ source: sourceName, march: queue[i] });
}
return;
}
if (typeof queue === 'object') {
for (var key in queue) {
if (!hasOwn(queue, key)) continue;
if (looksLikeIncomingMarch(queue[key])) out.push({ source: sourceName, march: queue[key] });
}
}
}
function getIncomingMarches() {
var sources = getIncomingQueueSources();
var items = [];
for (var i = 0; i < sources.length; i++) {
collectIncomingItems(sources[i].queue, sources[i].name, items);
}
return { sources: sources, items: items };
}
function getMarchKind(march) {
var raw = march.marchType != null ? march.marchType : (march.mt != null ? march.mt : march.type);
var n = parseInt(raw, 10);
if (n === 4) return 'attack';
if (n === 3) return 'scout';
var s = String(raw == null ? '' : raw).toLowerCase();
if (s.indexOf('attack') !== -1) return 'attack';
if (s.indexOf('scout') !== -1) return 'scout';
if (march.attackType != null) return 'attack';
if (march.scoutType != null) return 'scout';
return '';
}
function getMarchCity(march) {
var cityId = march.toCityId != null ? march.toCityId : (march.cityId != null ? march.cityId : march.cid);
var tileId = march.toTileId != null ? march.toTileId : (march.tileId != null ? march.tileId : march.tid);
var city = null;
if (cityId != null && tileId != null) {
city = Cities.byKey[String(cityId) + ':' + String(tileId)];
}
if (!city && cityId != null) {
city = Cities.byID[String(cityId)];
}
var x = march.toXCoord != null ? march.toXCoord : (march.xCoord != null ? march.xCoord : march.x);
var y = march.toYCoord != null ? march.toYCoord : (march.yCoord != null ? march.yCoord : march.y);
if (!city && x != null && y != null) {
city = Cities.byCoords[String(parseInt(x, 10)) + ':' + String(parseInt(y, 10))];
}
return city || null;
}
function getMarchFromCoords(march) {
var x = march.fromXCoord != null ? march.fromXCoord : (march.fromX != null ? march.fromX : (march.xCoordFrom != null ? march.xCoordFrom : '?'));
var y = march.fromYCoord != null ? march.fromYCoord : (march.fromY != null ? march.fromY : (march.yCoordFrom != null ? march.yCoordFrom : '?'));
return '(' + x + ',' + y + ')';
}
function getMarchPlayerName(march) {
var players = (unsafeWindow.seed && unsafeWindow.seed.players) ? unsafeWindow.seed.players : {};
var pid = march.pid != null ? march.pid : (march.playerId != null ? march.playerId : march.fromPlayerId);
var playerObj = players['u' + pid] || {};
return march.attackerName || march.playerName || march.fromPlayerName || playerObj.n || ('Player ' + (pid == null ? '?' : pid));
}
function getMarchUnits(march) {
var unitTypes = (unsafeWindow.cm && unsafeWindow.cm.UNIT_TYPES) ? unsafeWindow.cm.UNIT_TYPES : [];
var units = 0;
var parts = [];
var seen = {};
var hasUnitData = false;
function addUnit(unitType, count) {
count = parseInt(count, 10);
if (!(count > 0)) return;
hasUnitData = true;
units += count;
seen['u' + unitType] = true;
var label = 'u' + unitType;
try {
if (unsafeWindow.unitcost && unsafeWindow.unitcost['unt' + unitType]) {
label = unsafeWindow.unitcost['unt' + unitType][0] || label;
}
} catch (e) {}
parts.push(count + ' ' + label);
}
if (march.unts && typeof march.unts === 'object') {
for (var ui in unitTypes) {
if (!hasOwn(unitTypes, ui)) continue;
var unitType = unitTypes[ui];
addUnit(unitType, march.unts['u' + unitType]);
}
// Fallback for unit keys the game exposes but UNIT_TYPES does not list.
for (var key in march.unts) {
if (!hasOwn(march.unts, key)) continue;
if (seen[key]) continue;
if (/^u\d+$/i.test(key)) addUnit(key.replace(/^u/i, ''), march.unts[key]);
}
}
if (!hasUnitData) {
var fallbackFields = ['units', 'totalUnits', 'totalTroops', 'troops', 'numTroops', 'marchSize', 'count'];
for (var i = 0; i < fallbackFields.length; i++) {
var v = parseInt(march[fallbackFields[i]], 10);
if (v > 0) {
units = v;
hasUnitData = true;
parts.push(v + ' total troops');
break;
}
}
}
return {
units: units,
text: parts.length ? parts.join(' : ') : 'unknown troops',
hasUnitData: hasUnitData
};
}
function buildIncomingKey(source, march, city, kind) {
var pid = march.pid != null ? march.pid : (march.playerId != null ? march.playerId : march.fromPlayerId);
var from = getMarchFromCoords(march);
var eta = march.eta || march.arrivalTime || march.time || march.endTime || march.id || '';
return [kind, pid, city ? city.id : (march.toCityId || march.cityId || march.cid || '?'), march.toTileId || march.tileId || '?', from, eta].join('|');
}
function checkAutoPort() {
refreshCities();
var scanTime = new Date();
var errorText = '';
var found = getIncomingMarches();
var sources = found.sources;
var items = found.items;
var sourceText = sources.length ? sources.map(function (s) { return s.name; }).join(', ') : 'No incoming queue found';
var seen = {};
var incomingCount = 0;
var attackCount = 0;
var scoutCount = 0;
var matchedCount = 0;
var skippedCount = 0;
var lastThreatText = 'None';
var attackThreshold = parseInt(AutoPort.portThresholdAttack, 10);
var scoutThreshold = parseInt(AutoPort.portThresholdScout, 10);
if (isNaN(attackThreshold)) attackThreshold = 0;
if (isNaN(scoutThreshold)) scoutThreshold = 0;
var portEnabled = !!(AutoPort.portRefuge || AutoPort.portOrder);
if (!portEnabled) errorText = 'Port item not enabled';
for (var i = 0; i < items.length; i++) {
var source = items[i].source;
var march = items[i].march;
if (!march) continue;
var kind = getMarchKind(march);
if (kind !== 'attack' && kind !== 'scout') {
skippedCount++;
continue;
}
incomingCount++;
if (kind === 'attack') attackCount++;
if (kind === 'scout') scoutCount++;
var city = getMarchCity(march);
if (!city) {
skippedCount++;
if (!errorText) errorText = 'Incoming found but target city was not matched';
continue;
}
var dedupeKey = buildIncomingKey(source, march, city, kind);
if (seen[dedupeKey]) continue;
seen[dedupeKey] = true;
if (kind === 'attack' && !AutoPort.portCitiesAttack[city.idx]) {
skippedCount++;
continue;
}
if (kind === 'scout' && !AutoPort.portCitiesScout[city.idx]) {
skippedCount++;
continue;
}
var unitInfo = getMarchUnits(march);
if (kind === 'attack' && unitInfo.units < attackThreshold) {
skippedCount++;
continue;
}
if (kind === 'scout' && unitInfo.units < scoutThreshold) {
skippedCount++;
continue;
}
matchedCount++;
var attackerName = getMarchPlayerName(march);
var actionWord = kind === 'attack' ? 'attacked' : 'scouted';
var alertType = kind;
var fromCoords = getMarchFromCoords(march);
var attack_msg = city.name + ' at (' + city.x + ',' + city.y + ') ' + actionWord + ' by ' + attackerName + ' from ' + fromCoords + ' using [ : ' + unitInfo.text + ' ]';
lastThreatText = (kind === 'attack' ? 'Attack' : 'Scout') + ' on ' + city.name + ' from ' + fromCoords + ' / ' + unitInfo.units + ' troops';
setPortCoreDetectionStatus({
lastScanText: formatPortCoreTime(scanTime),
sourceText: sourceText,
incomingCount: incomingCount,
attackCount: attackCount,
scoutCount: scoutCount,
matchedCount: matchedCount,
skippedCount: skippedCount,
lastThreatText: lastThreatText,
lastErrorText: errorText || 'OK'
});
if (!portEnabled) return;
if (kind === 'attack' && AutoPort.discordEnabled && AutoPort.discordAttack) {
sendDiscordWebhook(
'🚨 ATTACK DETECTED 🚨',
buildDiscordEmbed('Incoming Attack', attack_msg + '\nTroops: ' + unitInfo.units + '\nSource: ' + source, 16711680)
);
}
if (kind === 'scout' && AutoPort.discordEnabled && AutoPort.discordScout) {
sendDiscordWebhook(
'👀 SCOUT DETECTED 👀',
buildDiscordEmbed('Incoming Scout', attack_msg + '\nTroops: ' + unitInfo.units + '\nSource: ' + source, 16753920)
);
}
if (AutoPort.portRefuge) {
PortOfRefugeCity(city.idx, march.pid, attack_msg, alertType);
return;
} else {
PortOfOrderCity(city.idx, march.pid, attack_msg, alertType);
return;
}
}
setPortCoreDetectionStatus({
lastScanText: formatPortCoreTime(scanTime),
sourceText: sourceText,
incomingCount: incomingCount,
attackCount: attackCount,
scoutCount: scoutCount,
matchedCount: matchedCount,
skippedCount: skippedCount,
lastThreatText: lastThreatText,
lastErrorText: errorText || 'OK'
});
}
function PortOfRefugeCity(citynum, pid, portmsg, alertType) {
var portParameters = unsafeWindow.Object.clone(unsafeWindow.g_ajaxparams);
portParameters.pf = 0;
portParameters.iid = 911;
portParameters.cid = unsafeWindow.seed.cities[citynum][0];
portParameters.pid = Math.floor((Math.random() * (24)) + 1);
var path = unsafeWindow.g_ajaxpath + "ajax/relocate.php" + unsafeWindow.g_ajaxsuffix;
new MyAjaxRequest(path, { method: "post", parameters: portParameters, onSuccess: function (rslt) {
if (rslt.ok) {
if (AutoPort.portWarning) sendChat('/a 2PIES PORT AUTOREPLY: ' + AutoPort.portWarningContent);
if (AutoPort.portSarcasm) sendChat('/' + unsafeWindow.seed.players['u' + pid].n + ' ' + AutoPort.portSarcasmContent);
if (AutoPort.portMessage) sendComposedMail(unsafeWindow.seed.player.name, "POR 2PIES PORT USED", portmsg);
if (AutoPort.discordEnabled && AutoPort.discordSuccess) {
sendDiscordWebhook(
"✅ AUTOPORT SUCCESS",
buildDiscordEmbed(
"Port of Refuge Used",
portmsg,
65280
)
);
}
setTimeout(function () { RefreshCamelot(); }, 1000);
} else {
if (AutoPort.discordEnabled && AutoPort.discordFail) {
sendDiscordWebhook(
"❌ AUTOPORT FAILED",
buildDiscordEmbed(
"Port of Refuge Failed",
portmsg + "\nCode " + rslt.error_code + ": " + rslt.msg,
10038562
)
);
}
sendComposedMail(unsafeWindow.seed.player.name, "POR FAILED", 'Code ' + rslt.error_code + ': ' + rslt.msg);
};
}, onFailure: function (rslt) {
if (AutoPort.discordEnabled && AutoPort.discordFail) {
sendDiscordWebhook(
"❌ AUTOPORT FAILED",
buildDiscordEmbed(
"Port of Refuge AJAX Failure",
portmsg,
10038562
)
);
}
}
});
}
function GetPortBlock() {
var province = 13;
while (province == 13) province = Math.floor((Math.random() * 25) + 1);
var x_off = 150 - ((Math.floor((Math.random() * 10))) * 15);
var y_off = 150 - ((Math.floor((Math.random() * 10))) * 15);
var x_multiplier = (province % 5) == 0 ? 5 : province % 5;
var y_multiplier = (province % 5) == 0 ? 1 : (((province - (province % 5)) / 5) + 1);
var xx = 150 * x_multiplier;
var yy = 150 * y_multiplier;
var xxx = xx - x_off < 0 ? 750 - xx - x_off : xx - x_off;
var yyy = yy - y_off < 0 ? 750 - yy - y_off : yy - y_off;
yyy = yyy > 750 ? 750 : yyy;
xxx = xxx > 750 ? 750 : xxx;
if (xxx >= 300 && xxx < 450 && yyy >= 300 && yyy < 450) {
xxx = 150;
yyy = 150;
}
return "bl_" + xxx + "_bt_" + yyy;
};
function PortOfOrderCity(citynum, pid, portmsg, alertType) {
var blockString = GetPortBlock();
var params = unsafeWindow.Object.clone(unsafeWindow.g_ajaxparams);
params.blocks = blockString;
new MyAjaxRequest(unsafeWindow.g_ajaxpath + "ajax/fetchMapTiles.php" + unsafeWindow.g_ajaxsuffix, {
method: "post",
parameters: params,
onSuccess: function (rslt) {
if (rslt.ok) {
var tcoMapData = rslt.data;
for (var tile in tcoMapData) {
if (parseInt(tcoMapData[tile].tileType) == 50 && (!tcoMapData[tile].tileUserId || tcoMapData[tile].tileUserId==0 || tcoMapData[tile].tileUserId=="0")) {
var portParameters = unsafeWindow.Object.clone(unsafeWindow.g_ajaxparams);
portParameters.pf = 0;
portParameters.iid = 912;
portParameters.cid = unsafeWindow.seed.cities[citynum][0];
portParameters.xcoord = tcoMapData[tile].xCoord;
portParameters.ycoord = tcoMapData[tile].yCoord;
var path = unsafeWindow.g_ajaxpath + "ajax/relocate.php" + unsafeWindow.g_ajaxsuffix;
new MyAjaxRequest(path, {
method: "post",
parameters: portParameters,
onSuccess: function (rslt) {
if (rslt.ok) {
if (AutoPort.portWarning) sendChat('/a 2PIES PORT AUTOREPLY: ' + AutoPort.portWarningContent);
if (AutoPort.portSarcasm) sendChat('/' + unsafeWindow.seed.players['u' + pid].n + ' ' + AutoPort.portSarcasmContent);
if (AutoPort.portMessage) sendComposedMail(unsafeWindow.seed.player.name, "POO 2PIES PORT USED", portmsg);
if (AutoPort.discordEnabled && AutoPort.discordSuccess) {
sendDiscordWebhook(
"✅ AUTOPORT SUCCESS",
buildDiscordEmbed(
"Port of Order Used",
portmsg + "\nNew Location: (" + tcoMapData[tile].xCoord + "," + tcoMapData[tile].yCoord + ")",
65280
)
);
}
setTimeout(function () { RefreshCamelot(); }, 1000);
} else {
if (rslt.error_code == 104) {
setTimeout(function () { PortOfOrderCity(citynum, pid, portmsg, alertType); }, 0);
return;
} else {
if (AutoPort.discordEnabled && AutoPort.discordFail) {
sendDiscordWebhook(
"❌ AUTOPORT FAILED",
buildDiscordEmbed(
"Port of Order Failed",
portmsg + "\nCode " + rslt.error_code + ": " + rslt.msg,
10038562
)
);
}
sendComposedMail(unsafeWindow.seed.player.name, "POO FAILED", 'Code ' + rslt.error_code + ': ' + rslt.msg);
}
};
},
onFailure: function (rslt) {
if (AutoPort.discordEnabled && AutoPort.discordFail) {
sendDiscordWebhook(
"❌ AUTOPORT FAILED",
buildDiscordEmbed(
"Port of Order AJAX Failure",
portmsg,
10038562
)
);
}
setTimeout(function () { PortOfOrderCity(citynum, pid, portmsg, alertType); }, 1000);
}
});
return;
}
}
}
},
onFailure: function (rslt) {
setTimeout(function () { PortOfOrderCity(citynum, pid, portmsg, alertType); }, 1000);
}
}, false);
}
/* var PortCoreAutoUpdater = {
id: 'PortCore2015',
name: '2Pies Port',
version: Version,
secure: true,
call: function (secure, response) {
logit("Checking for " + this.name + " Update!" + (secure ? ' (SSL)' : ' (plain)'));
this.secure = secure;
PortCoreHttpRequest({
method: 'GET',
url: 'https://greasyfork.org/scripts/21170-ne0-auto-port/code/2Pies Port.user.js',
onload: function (xpr) { PortCoreAutoUpdater.compare(xpr, response); },
onerror: function (xpr) { if (secure) { PortCoreAutoUpdater.call(false, response); } else { PortCoreAutoUpdater.compare({ responseText: "" }, response); } }
});
},
compareVersion: function (r_version, l_version) {
var r_parts = r_version.split(''),
l_parts = l_version.split(''),
r_len = r_parts.length,
l_len = l_parts.length,
r = l = 0;
for (var i = 0, len = (r_len > l_len ? r_len : l_len); i < len && r == l; ++i) {
r = +(r_parts[i] || '0');
l = +(l_parts[i] || '0');
}
return (r !== l) ? r > l : false;
},
compare: function (xpr, response) {
this.xversion = /\/\/\s*@version\s+(.+)\s*\n/i.exec(xpr.responseText);
if (this.xversion) this.xversion = this.xversion[1];
else {
if (response) {
unsafeWindow.Modal.showAlert('<div align="center">Unable to check for updates</div>');
}
logit("Unable to check for updates :(");
return;
}
this.xrelnotes = /\/\/\s*@releasenotes\s+(.+)\s*\n/i.exec(xpr.responseText);
if (this.xrelnotes) this.xrelnotes = this.xrelnotes[1];
var updated = this.compareVersion(this.xversion, this.version);
if (updated) {
logit('New Version Available!');
var body = '<BR><DIV align=center><FONT size=3><B>New version ' + this.xversion + ' is available!</b></font></div><BR>';
if (this.xrelnotes)
body += '<BR><div align="center" style="border:0;width:470px;height:120px;max-height:120px;overflow:auto"><b>New Features!</b><p>' + this.xrelnotes + '</p></div><BR>';
body += '<BR><DIV align=center><a class="gemButtonv2 green" id="PortCoredoBotUpdate">Update</a></div>';
PortCoreShowUpdate(body);
}
else {
logit("No updates available :(");
if (response) {
unsafeWindow.Modal.showAlert('<div align="center">No updates available at this time.</div>');
}
}
},
check: function () {
var now = unixTime();
var lastCheck = 0;
if (GM_getValue('updated_' + this.id, 0)) lastCheck = parseInt(GM_getValue('updated_' + this.id, 0));
if (now > (lastCheck + 60 * 60 * 12)) this.call(true, false);
}
}; */
/* function PortCoredoBOTUpdate() {
unsafeWindow.cm.ModalManager.closeAll();
unsafeWindow.cm.ModalManager.close();
var now = unixTime();
GM_setValue('updated_' + PortCoreAutoUpdater.id, now);
location.href = 'https://greasyfork.org/scripts/21170-ne0-auto-port/code/2Pies Port.user.js';
} */
/* function PortCoreShowUpdate(body) {
var now = unixTime();
unsafeWindow.cm.ModalManager.addMedium({
title: PortCoreAutoUpdater.name,
body: body,
closeNow: false,
close: function () {
GM_setValue('updated_' + PortCoreAutoUpdater.id, now);
unsafeWindow.cm.ModalManager.closeAll();
},
"class": "Warning",
curtain: false,
width: 500,
height: 700,
left: 140,
top: 140
});
document.getElementById('PortCoredoBotUpdate').addEventListener('click', PortCoredoBOTUpdate, false);
}
*/
function addUrlArgs(url, params) {
if (!params) return url;
var pairs = [];
for (var key in params) {
if (!hasOwn(params, key)) continue;
if (params[key] == null) continue;
pairs.push(encodeURIComponent(key) + '=' + encodeURIComponent(params[key]));
}
if (!pairs.length) return url;
return url + (url.indexOf('?') === -1 ? '?' : '&') + pairs.join('&');
}
function AjaxRequest(url, opts) { // emulate Prototype's Ajax.Request with safer modern XHR handling
opts = opts || {};
const headers = {
'X-Requested-With': 'XMLHttpRequest',
'X-Prototype-Version': '1.6.1',
'Accept': 'text/javascript, text/html, application/xml, text/xml, */*'
};
const method = (opts.method == null || opts.method === '') ? 'GET' : String(opts.method).toUpperCase();
if (method === 'POST') {
headers['Content-type'] = 'application/x-www-form-urlencoded; charset=UTF-8';
} else if (method === 'GET') {
url = addUrlArgs(url, opts.parameters);
}
const ajax = window.XMLHttpRequest ? new XMLHttpRequest() : new ActiveXObject('Microsoft.XMLHTTP');
ajax.onreadystatechange = function () {
if (ajax.readyState === 4) {
if (ajax.status >= 200 && ajax.status < 305) {
if (opts.onSuccess) opts.onSuccess(ajax);
} else if (opts.onFailure) {
opts.onFailure(ajax);
}
} else if (opts.onChange) {
opts.onChange(ajax);
}
};
ajax.open(method, url, true);
Object.keys(headers).forEach(function (key) {
ajax.setRequestHeader(key, headers[key]);
});
if (matTypeof(opts.requestHeaders) === 'object') {
Object.keys(opts.requestHeaders).forEach(function (key) {
ajax.setRequestHeader(key, opts.requestHeaders[key]);
});
}
if (method === 'POST') {
ajax.send(encodePostParameters(opts.parameters || {}));
} else {
ajax.send();
}
}
function encodePostParameters(params) {
const parts = [];
Object.keys(params || {}).forEach(function (key) {
const value = params[key];
if (matTypeof(value) === 'object') {
Object.keys(value).forEach(function (childKey) {
parts.push(encodeURIComponent(key + '[' + childKey + ']') + '=' + encodeURIComponent(value[childKey]));
});
} else {
parts.push(encodeURIComponent(key) + '=' + encodeURIComponent(value));
}
});
return parts.join('&');
}
function MyAjaxRequest (url, o) {
if (DEBUG_TRACE) logit (" 0 myAjaxRequest: "+ url +"\n" + inspect (o, 2, 1));
var opts = unsafeWindow.Object.clone(o);
var wasSuccess = o.onSuccess;
var wasFailure = o.onFailure;
opts.onSuccess = mySuccess;
opts.onFailure = myFailure;
new AjaxRequest(url, opts);
return;
function myFailure(){
var o = {};
o.ok = false;
o.errorMsg = "AJAX Communication Failure";
if (wasFailure) wasFailure(o);
}
function mySuccess (msg){
var rslt;
try {
rslt = JSON2.parse(msg.responseText);
} catch (e) {
logit("Message parse error: " + e + "\n" + inspect(msg, 3, 1));
return;
}
if (!rslt) {
logit("Message error: " + inspect(msg, 3, 1));
return;
}
if (window.EmulateAjaxError){
rslt.ok = false;
rslt.error_code=8;
}
if (rslt.ok){
if (rslt.updateSeed)
unsafeWindow.update_seed(rslt.updateSeed);
wasSuccess (rslt);
return;
}
rslt.errorMsg = unsafeWindow.printLocalError((rslt.error_code || null), (rslt.msg || null), (rslt.feedback || null));
wasSuccess (rslt);
}
}
function PortCoreScriptStartup () {
var metc = getClientCoords(document.getElementById('main_engagement_tabs'));
if (metc.width==null || metc.width==0) {
setTimeout (PortCoreScriptStartup, 1000);
return;
}
// initialise
var styles = [
'.PortCoreCPopup{border:2px solid #d6aa32!important;border-radius:18px!important;background:#fffaf0!important;box-shadow:0 18px 60px rgba(8,38,74,.38)!important;overflow:hidden;font-family:Arial,Helvetica,sans-serif!important;}',
'.PortCoreCPopup table{border-collapse:collapse;}',
'tr.CPopupTop td{background:linear-gradient(180deg,#08264a,#03162e)!important;border:0!important;height:34px!important;color:#f6d56d!important;}',
'#PortCoreMain_X{background:#03162e!important;color:#f6d56d!important;border-left:1px solid #d6aa32!important;}',
'.portCoreTopShell{display:flex;align-items:center;gap:10px;width:100%;box-sizing:border-box;padding:4px 8px;color:#f6d56d;}',
'.portCoreTopBrand{font-size:15px;font-weight:900;letter-spacing:.4px;color:#f6d56d;text-shadow:0 1px 0 rgba(0,0,0,.35);}',
'.portCoreTopSub{font-size:10px;color:#ffe79a;opacity:.92;}',
'.portCoreTopTabs{margin-left:auto;display:flex;gap:6px;}',
'.portCoreTopTab{display:inline-block;padding:3px 8px;border-radius:8px;border:1px solid rgba(214,170,50,.6);color:#f6d56d;cursor:pointer;font-size:10px;}',
'.portCoreTopTab.active,.portCoreTopTab:hover{background:rgba(246,213,109,.16);color:#ffe79a;}',
'.CPopMain{background:#fffaf0!important;border:0!important;color:#2f260b!important;font-size:12px!important;box-shadow:inset 0 1px 0 rgba(255,255,255,.85)!important;border-radius:0 0 18px 18px!important;overflow:auto!important;}',
'#PortCoreMain_content{height:665px;max-height:665px;overflow:auto;padding-right:4px;box-sizing:border-box;}',
'.portDash{box-sizing:border-box;padding:8px;background:#fffaf0;min-height:100%;height:auto;overflow:visible;color:#2f260b;}',
'.portHero{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:8px;padding:9px;border:1px solid #d6aa32;border-radius:16px;background:linear-gradient(135deg,#ffffff,#fff0b8);box-shadow:0 10px 32px rgba(160,116,0,.16);}',
'.portTitle{font-size:18px;font-weight:800;letter-spacing:.3px;color:#5c4100;}',
'.portSub{font-size:10px;color:#7b641e;margin-top:2px;}',
'.portBadge{display:inline-block;min-width:92px;text-align:center;border-radius:999px;padding:5px 9px;font-weight:800;font-size:12px;background:#d6aa32;color:#251900;border:1px solid #a87b00;box-shadow:0 2px 8px rgba(168,123,0,.22);}',
'.portBadge.off{background:#fff5cf;color:#7b641e;border-color:#d6aa32;}',
'.portStats{display:grid;grid-template-columns:repeat(4,1fr);gap:7px;margin-bottom:8px;}',
'.portStat{border:1px solid #e0bd54;background:#ffffff;border-radius:11px;padding:7px;box-shadow:0 6px 18px rgba(160,116,0,.12);}',
'.portStatLabel{font-size:10px;text-transform:uppercase;letter-spacing:.8px;color:#8a6f1b;}',
'.portStatValue{font-size:16px;font-weight:800;color:#5c4100;margin-top:1px;}',
'.portGrid{display:grid;grid-template-columns:1fr 1fr;gap:6px;align-items:start;}',
'.portCard{border:1px solid #e0bd54;border-radius:12px;background:#ffffff;padding:8px;box-shadow:0 8px 24px rgba(160,116,0,.14);}',
'.portCityCard{grid-column:1 / -1;}',
'.portCard h3{margin:0 0 6px 0;font-size:12px;color:#5c4100;text-transform:uppercase;letter-spacing:.8px;border-bottom:1px solid #efd37a;padding-bottom:5px;}',
'.portRow{display:flex;align-items:center;justify-content:space-between;gap:10px;margin:4px 0;color:#3b2a00;}',
'.portRow label{font-weight:bold;}',
'.portInput,.portDash input[type=text],.portDash input[type=password]{box-sizing:border-box;background:#fffef8;border:1px solid #d6aa32;color:#2f260b;border-radius:8px;padding:5px 7px;outline:none;}',
'.portDash input[type=text]:focus,.portDash input[type=password]:focus{border-color:#b98a10;box-shadow:0 0 0 2px rgba(214,170,50,.24);}',
'.portSmallInput{width:78px;text-align:center;}',
'.portFullInput{width:100%;}',
'.portCheck{display:flex;align-items:center;gap:6px;color:#3b2a00;}',
'.portCheck input{transform:scale(1.08);accent-color:#d6aa32;}',
'.portDash input[type=checkbox]{accent-color:#d6aa32;}',
'.portCityBox{max-height:360px;overflow-y:auto;overflow-x:hidden;border-radius:12px;border:1px solid #d6aa32;background:#fffef8;}',
'.portCityTable{width:100%;border-collapse:collapse;table-layout:fixed;}',
'.portCityTable th{position:sticky;top:0;background:#f6d56d;color:#3b2a00;font-size:10px;text-transform:uppercase;letter-spacing:.6px;padding:6px;z-index:1;border-bottom:1px solid #b98a10;}',
'.portCityTable td{padding:5px 6px;border-top:1px solid #f1dc98;color:#2f260b;}',
'.portCityTable tr:nth-child(even) td{background:#fff8df;}',
'.portCityTable tr:nth-child(odd) td{background:#ffffff;}',
'.portBtnBar{display:flex;flex-wrap:wrap;gap:6px;margin-top:7px;}',
'.portBtn{border:1px solid #b98a10;background:#d6aa32;color:#251900;border-radius:8px;padding:5px 8px;font-weight:bold;cursor:pointer;box-shadow:0 2px 7px rgba(168,123,0,.16);}',
'.portBtn:hover{background:#ffe79a;color:#241900;}',
'.portBtn.secondary{background:#fff3c4;border-color:#d6aa32;color:#3b2a00;}',
'.portBtn.danger{background:#f2d37a;border-color:#b98a10;color:#3b2a00;}',
'.portNote{font-size:11px;color:#7b641e;line-height:1.25;margin-top:5px;}',
'.portTip{display:inline-block;position:relative;margin-left:5px;width:15px;height:15px;line-height:15px;text-align:center;border-radius:50%;background:#08264a;color:#f6d56d;border:1px solid #d6aa32;font-size:10px;font-weight:bold;cursor:help;vertical-align:middle;}',
'.portTipBox{display:none;position:absolute;left:18px;top:-8px;width:255px;z-index:999999;background:#fff8df;color:#3b2a00;border:1px solid #d6aa32;border-radius:10px;padding:8px;text-align:left;line-height:1.35;font-weight:normal;box-shadow:0 8px 22px rgba(8,38,74,.22);}',
'.portTip:hover .portTipBox{display:block;}',
'.portTipBox b{color:#08264a;}',
'.portTwo{display:grid;grid-template-columns:1fr 1fr;gap:6px;}',
'.portCompactHint{font-size:10px;color:#8a6f1b;margin-top:4px;}',
'.portCityTable th:nth-child(2),.portCityTable th:nth-child(3),.portCityTable td:nth-child(2),.portCityTable td:nth-child(3){width:90px;text-align:center;}',
'.portStatusLine{font-size:11px;color:#7b641e;margin-left:8px;}',
'.portDetectGrid{display:grid;grid-template-columns:repeat(5,1fr);gap:6px;}',
'.portDetectItem{border:1px solid #efd37a;border-radius:10px;background:#fffef8;padding:6px;min-height:38px;}',
'.portDetectLabel{font-size:9px;text-transform:uppercase;letter-spacing:.6px;color:#8a6f1b;}',
'.portDetectValue{font-size:11px;font-weight:bold;color:#3b2a00;margin-top:2px;word-break:break-word;}',
'.portUpdateStatus{font-size:11px;color:#7b641e;margin-left:8px;}',
'.portUpdateStatus.good{color:#0b6b2f;font-weight:bold;}',
'.portUpdateStatus.warn{color:#9a6a00;font-weight:bold;}',
'.portUpdateStatus.bad{color:#9b1c1c;font-weight:bold;}',
'@media(max-width:820px){.portStats{grid-template-columns:repeat(2,1fr);}.portGrid{grid-template-columns:1fr;}.portCityCard{grid-column:auto;}.portDetectGrid{grid-template-columns:1fr 1fr;}}',
].join('');
LoadAutoPortData();
refreshCities();
AddMainTabLink('2Pies Port', eventHideShow);
PortCoremainPop = new PortCoreCPopup ('PortCoreMain', 0, 0, dlgWidth, dlgHeight, true, function (){ PortCoretabManager.hideTab(); });
PortCoremainPop.getMainDiv().innerHTML = '<STYLE>'+ styles +'</style>';
PortCoretabManager.init (PortCoremainPop.getMainDiv());
startAutoPortLoop();
//setTimeout(function(){PortCoreAutoUpdater.check();},15000);
}
function comparePortCoreVersions(remoteVersion, localVersion) {
function parts(v) {
var m = String(v || '').match(/[0-9]+(?:\.[0-9]+)*/);
return m ? m[0].split('.').map(function (x) { return parseInt(x, 10) || 0; }) : [0];
}
var r = parts(remoteVersion);
var l = parts(localVersion);
var max = Math.max(r.length, l.length);
for (var i = 0; i < max; i++) {
var rv = r[i] || 0;
var lv = l[i] || 0;
if (rv > lv) return 1;
if (rv < lv) return -1;
}
return 0;
}
function setPortUpdateStatus(message, state) {
var el = document.getElementById('portUpdateStatus');
if (!el) return;
el.innerHTML = message;
el.className = 'portUpdateStatus' + (state ? ' ' + state : '');
}
function getPortUpdateUrlFromUi() {
var el = document.getElementById('portUpdateUrl');
return el ? String(el.value || '').replace(/^\s+|\s+$/g, '') : String(AutoPort.updateUrl || '');
}
function checkPortCoreUpdate() {
var url = getPortUpdateUrlFromUi();
AutoPort.updateUrl = url;
SaveAutoPortData(true);
if (!url) {
setPortUpdateStatus('Paste a raw .user.js update URL first', 'warn');
return;
}
setPortUpdateStatus('Checking...', '');
try {
PortCoreHttpRequest({
method: 'GET',
url: url,
onload: function (res) {
var body = res && res.responseText ? res.responseText : '';
var match = /\/\/\s*@version\s+([^\r\n]+)/i.exec(body);
if (!match) {
setPortUpdateStatus('No @version found at URL', 'bad');
return;
}
var remoteVersion = match[1].replace(/^\s+|\s+$/g, '');
var cmp = comparePortCoreVersions(remoteVersion, Version);
if (cmp > 0) {
setPortUpdateStatus('New version found: ' + remoteVersion, 'good');
} else if (cmp === 0) {
setPortUpdateStatus('Already current: ' + remoteVersion, 'good');
} else {
setPortUpdateStatus('Local is newer than URL: ' + remoteVersion, 'warn');
}
},
onerror: function () {
setPortUpdateStatus('Update check failed', 'bad');
}
});
} catch (e) {
setPortUpdateStatus('Update check error', 'bad');
if (DEBUG_TRACE) logit('checkPortCoreUpdate error: ' + e);
}
}
function openPortCoreUpdateUrl() {
var url = getPortUpdateUrlFromUi();
AutoPort.updateUrl = url;
SaveAutoPortData(true);
if (!url) {
setPortUpdateStatus('Paste an update URL first', 'warn');
return;
}
try {
window.open(url, '_blank');
setPortUpdateStatus('Opened update URL', 'good');
} catch (e) {
location.href = url;
}
}
function openDiscordWebhookVideoGuide() {
try {
window.open(DISCORD_WEBHOOK_VIDEO_URL, '_blank');
var status = document.getElementById('discordTestStatus');
if (status) status.innerHTML = 'Opened video guide';
} catch (e) {
location.href = DISCORD_WEBHOOK_VIDEO_URL;
}
}
/*********************************** TABS ***********************************/
Tabs.MainTab = {
tabOrder: 100,
tabLabel: 'AutoPort - version ' + Version,
tabColor: 'red',
rendered: false,
renderedCityCount: -1,
mydiv: null,
init: function (div) {
var t = Tabs.MainTab;
t.mydiv = div;
t.rendered = false;
t.renderedCityCount = -1;
},
hide: function () {},
show: function () {
var t = Tabs.MainTab;
refreshCities();
if (t.rendered && t.renderedCityCount !== Cities.numCities) {
t.mydiv.innerHTML = "";
t.rendered = false;
}
if (!t.rendered) {
t.render();
t.bindEvents();
t.rendered = true;
t.renderedCityCount = Cities.numCities;
}
t.refreshState();
},
esc: function (v) {
return String(v == null ? '' : v)
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
},
getItemCount: function (iid) {
try {
if (unsafeWindow.ksoItems && unsafeWindow.ksoItems[iid]) return parseInt(unsafeWindow.ksoItems[iid].count, 10) || 0;
} catch (e) {}
return 0;
},
render: function () {
var t = Tabs.MainTab;
var maxCities = getCitySlotCount();
var m = '';
m += '<div id="portDash" class="portDash">';
m += ' <div class="portHero">';
m += ' <div><span id="portArmedBadge" class="portBadge">ARMED</span><div id="portLoopText" class="portSub" style="text-align:right;margin-top:7px">Loading...</div></div>';
m += ' </div>';
m += ' <div class="portStats">';
m += ' <div class="portStat"><div class="portStatLabel">Cities loaded</div><div id="portStatCitiesLoaded" class="portStatValue">0/8</div></div>';
m += ' <div class="portStat"><div class="portStatLabel">Attack trigger</div><div id="portStatAttack" class="portStatValue">0</div></div>';
m += ' <div class="portStat"><div class="portStatLabel">Attack cities</div><div id="portStatAttackCities" class="portStatValue">0</div></div>';
m += ' <div class="portStat"><div class="portStatLabel">Scout cities</div><div id="portStatScoutCities" class="portStatValue">0</div></div>';
m += ' </div>';
m += ' <div class="portGrid">';
m += ' <div class="portCard portCityCard">';
m += ' <h3>Detection Status</h3>';
m += ' <div class="portDetectGrid">';
m += ' <div class="portDetectItem"><div class="portDetectLabel">Last scan</div><div id="portDetectLastScan" class="portDetectValue">Never</div></div>';
m += ' <div class="portDetectItem"><div class="portDetectLabel">Source</div><div id="portDetectSource" class="portDetectValue">Not scanned yet</div></div>';
m += ' <div class="portDetectItem"><div class="portDetectLabel">Incoming</div><div id="portDetectIncoming" class="portDetectValue">0 total</div></div>';
m += ' <div class="portDetectItem"><div class="portDetectLabel">Matched</div><div id="portDetectMatched" class="portDetectValue">0 matched</div></div>';
m += ' <div class="portDetectItem"><div class="portDetectLabel">Last threat</div><div id="portDetectThreat" class="portDetectValue">None</div></div>';
m += ' </div>';
m += ' <div class="portNote">Detection reads the normal KoC incoming queue first, then checks likely fallback incoming queues from the game seed. If this says no incoming queue found, the game has not exposed incoming data to the page yet.</div>';
m += ' <div class="portNote"><b>Status:</b> <span id="portDetectError">OK</span></div>';
m += ' </div>';
m += ' <div class="portCard">';
m += ' <h3>Core AutoPort Settings</h3>';
m += ' <div class="portTwo">';
m += ' <div class="portRow"><label>Attack troops ≥</label><input id="portThresholdAttack" class="portSmallInput" type="text" value="' + t.esc(AutoPort.portThresholdAttack) + '" /></div>';
m += ' <div class="portRow"><label>Scout troops ≥</label><input id="portThresholdScout" class="portSmallInput" type="text" value="' + t.esc(AutoPort.portThresholdScout) + '" /></div>';
m += ' </div>';
m += ' <div class="portTwo">';
m += ' <label class="portCheck"><input id="portRefuge" type="checkbox" ' + (AutoPort.portRefuge ? 'checked' : '') + ' /> Use Port of Refuge <b id="portRefugeCount">0</b></label>';
m += ' <label class="portCheck"><input id="portOrder" type="checkbox" ' + (AutoPort.portOrder ? 'checked' : '') + ' /> Use Port of Order <b id="portOrderCount">0</b></label>';
m += ' </div>';
m += ' <div class="portRow"><label>Recheck every</label><span><input id="portRecheck" class="portSmallInput" type="text" value="' + t.esc(AutoPort.portRecheck) + '" /> seconds</span></div>';
m += ' <div class="portNote">Tip: lower recheck is faster, but 2 seconds minimum is enforced so the page does not choke.</div>';
m += ' </div>';
m += ' <div class="portCard">';
m += ' <h3>Discord Webhook <span class="portTip">?<span class="portTipBox"><b>How to set it up:</b><br>Discord → Server Settings → Integrations → Webhooks → New Webhook → choose channel → Copy Webhook URL → paste it here → tick Enable Discord → Test webhook.<br><br><b>Video:</b> Press the Video Guide button below for a YouTube walkthrough.<br><br><b>Tip:</b> Keep your webhook URL private.</span></span></h3>';
m += ' <label class="portCheck"><input id="discordEnabled" type="checkbox" ' + (AutoPort.discordEnabled ? 'checked' : '') + ' /> Enable Discord</label>';
m += ' <div class="portRow" style="display:block"><label>Webhook URL</label><input id="discordWebhook" class="portFullInput" type="text" title="Paste your Discord webhook URL here, then tick Enable Discord and press Test Webhook." value="' + t.esc(AutoPort.discordWebhook) + '" /></div>';
m += ' <div class="portNote">Need help? Hover the ? or press <b>Video Guide</b> for a YouTube walkthrough.</div>';
m += ' <div class="portRow" style="display:block"><label>Mention</label><input id="discordMention" class="portFullInput" type="text" value="' + t.esc(AutoPort.discordMention) + '" placeholder="@everyone or <@userid>" /></div>';
m += ' <div class="portTwo">';
m += ' <label class="portCheck"><input id="discordAttack" type="checkbox" ' + (AutoPort.discordAttack ? 'checked' : '') + ' /> Attack</label>';
m += ' <label class="portCheck"><input id="discordScout" type="checkbox" ' + (AutoPort.discordScout ? 'checked' : '') + ' /> Scout</label>';
m += ' <label class="portCheck"><input id="discordSuccess" type="checkbox" ' + (AutoPort.discordSuccess ? 'checked' : '') + ' /> Success</label>';
m += ' <label class="portCheck"><input id="discordFail" type="checkbox" ' + (AutoPort.discordFail ? 'checked' : '') + ' /> Fail</label>';
m += ' </div>';
m += ' <div class="portBtnBar"><button id="discordTest" class="portBtn" type="button">Test webhook</button><button id="discordVideoGuide" class="portBtn secondary" type="button" title="Open a YouTube walkthrough for creating a Discord webhook">Video Guide</button><button id="portManualScan" class="portBtn secondary" type="button">Run scan now</button><button id="portSaveNow" class="portBtn secondary" type="button">Save now</button><span id="discordTestStatus" class="portStatusLine">Ready</span></div>';
m += ' </div>';
m += ' <div class="portCard portCityCard">';
m += ' <h3>Cities To AutoPort</h3>';
m += ' <div class="portNote">This panel always shows 8 city slots. If a slot says not loaded, refresh the game after all cities have loaded.</div>';
m += ' <div class="portCityBox"><table class="portCityTable"><thead><tr><th>City</th><th>Coords</th><th>Attack</th><th>Scout</th></tr></thead><tbody>';
for (var i = 0; i < maxCities; i++) {
var c = Cities.cities[i];
var name = c ? c.name : ('City ' + (i + 1) + ' not loaded');
var coords = c ? ('(' + t.esc(c.x) + ',' + t.esc(c.y) + ')') : 'refresh game';
m += '<tr>';
m += '<td><b>' + t.esc(name) + '</b></td>';
m += '<td>' + coords + '</td>';
m += '<td align="center"><input id="portCityAttack' + i + '" class="portCityAttack" type="checkbox" ' + (AutoPort.portCitiesAttack[i] ? 'checked' : '') + ' /></td>';
m += '<td align="center"><input id="portCityScout' + i + '" class="portCityScout" type="checkbox" ' + (AutoPort.portCitiesScout[i] ? 'checked' : '') + ' /></td>';
m += '</tr>';
}
m += ' </tbody></table></div>';
m += ' <div class="portBtnBar"><button id="portAllAttacks" class="portBtn secondary" type="button">All attacks</button><button id="portAllScouts" class="portBtn secondary" type="button">All scouts</button><button id="portClearAttacks" class="portBtn secondary" type="button">Clear attacks</button><button id="portClearScouts" class="portBtn secondary" type="button">Clear scouts</button><button id="portClearAll" class="portBtn danger" type="button">Clear all</button></div>';
m += ' </div>';
m += ' <div class="portCard portCityCard">';
m += ' <h3>Alerts & Replies</h3>';
m += ' <div class="portTwo">';
m += ' <div><label class="portCheck"><input id="portWarning" type="checkbox" ' + (AutoPort.portWarning ? 'checked' : '') + ' /> Warn alliance chat</label><input id="portWarningContent" class="portFullInput" type="text" value="' + t.esc(AutoPort.portWarningContent) + '" /></div>';
m += ' <div><label class="portCheck"><input id="portSarcasm" type="checkbox" ' + (AutoPort.portSarcasm ? 'checked' : '') + ' /> Sarcastic whisper</label><input id="portSarcasmContent" class="portFullInput" type="text" value="' + t.esc(AutoPort.portSarcasmContent) + '" /></div>';
m += ' </div>';
m += ' <div style="margin-top:8px">';
m += ' <label class="portCheck"><input id="portMessage" type="checkbox" ' + (AutoPort.portMessage ? 'checked' : '') + ' /> Mail port details to inbox</label>';
m += ' </div>';
m += ' </div>';
m += ' <div class="portCard portCityCard">';
m += ' <h3>Update Options</h3>';
m += ' <div class="portTwo">';
m += ' <div class="portRow"><label>Current version</label><b>' + t.esc(Version) + '</b></div>';
m += ' <div class="portRow"><label>Update mode</label><b>Manual URL check</b></div>';
m += ' </div>';
m += ' <div class="portRow" style="display:block"><label>Raw .user.js update URL</label><input id="portUpdateUrl" class="portFullInput" type="text" title="Paste a raw userscript URL here if you host this custom build somewhere." value="' + t.esc(AutoPort.updateUrl || '') + '" /></div>';
m += ' <div class="portBtnBar"><button id="portCheckUpdate" class="portBtn secondary" type="button">Check update</button><button id="portOpenUpdate" class="portBtn secondary" type="button">Open update URL</button><span id="portUpdateStatus" class="portUpdateStatus">Ready</span></div>';
m += ' <div class="portNote">Because this is a custom 2Pies Port build, auto-update only works if you host the latest .user.js somewhere and paste that raw URL here.</div>';
m += ' </div>';
m += ' </div>';
m += '</div>';
t.mydiv.innerHTML = m;
},
bindEvents: function () {
var t = Tabs.MainTab;
t.mydiv.addEventListener('change', function (e) {
t.handleChange(e.target || e.srcElement);
}, false);
t.mydiv.addEventListener('click', function (e) {
t.handleClick(e.target || e.srcElement);
}, false);
},
setCheckedByClass: function (className, checked) {
var nodes = document.getElementsByClassName(className);
for (var i = 0; i < nodes.length; i++) nodes[i].checked = checked;
},
refreshState: function () {
var t = Tabs.MainTab;
var refugeCount = t.getItemCount(911);
var orderCount = t.getItemCount(912);
if (refugeCount === 0 && AutoPort.portRefuge) AutoPort.portRefuge = false;
if (orderCount === 0 && AutoPort.portOrder) AutoPort.portOrder = false;
var attackCities = 0;
var scoutCities = 0;
var maxCities = getCitySlotCount();
for (var i = 0; i < maxCities; i++) {
if (AutoPort.portCitiesAttack[i]) attackCities++;
if (AutoPort.portCitiesScout[i]) scoutCities++;
}
function setHtml(id, val) {
var el = document.getElementById(id);
if (el) el.innerHTML = val;
}
function setCheck(id, val) {
var el = document.getElementById(id);
if (el) el.checked = !!val;
}
setHtml('portStatAttack', t.esc(AutoPort.portThresholdAttack));
setHtml('portStatScout', t.esc(AutoPort.portThresholdScout));
setHtml('portStatCitiesLoaded', Cities.numCities + '/' + maxCities);
setHtml('portStatAttackCities', attackCities + '/' + maxCities);
setHtml('portStatScoutCities', scoutCities + '/' + maxCities);
setHtml('portRefugeCount', '(' + refugeCount + ')');
setHtml('portOrderCount', '(' + orderCount + ')');
setCheck('portRefuge', AutoPort.portRefuge);
setCheck('portOrder', AutoPort.portOrder);
var armed = (AutoPort.portRefuge || AutoPort.portOrder) && (attackCities > 0 || scoutCities > 0);
var badge = document.getElementById('portArmedBadge');
if (badge) {
badge.innerHTML = armed ? 'ARMED' : 'STANDBY';
badge.className = armed ? 'portBadge' : 'portBadge off';
}
var loopText = document.getElementById('portLoopText');
if (loopText) loopText.innerHTML = 'Scan every ' + getPortRecheckSeconds() + 's';
updatePortCoreDetectionUi();
},
normalizeNumberInput: function (id, min, fallback, max) {
var el = document.getElementById(id);
var v = el ? parseInt(el.value, 10) : fallback;
if (isNaN(v) || v < min) v = fallback;
if (max != null && v > max) v = max;
if (el) el.value = v;
return v;
},
handleChange: function (el) {
if (!el || !el.id) return;
var t = Tabs.MainTab;
var cbMap = {
portRefuge: 'portRefuge',
portOrder: 'portOrder',
portWarning: 'portWarning',
portSarcasm: 'portSarcasm',
portMessage: 'portMessage',
discordEnabled: 'discordEnabled',
discordAttack: 'discordAttack',
discordScout: 'discordScout',
discordSuccess: 'discordSuccess',
discordFail: 'discordFail'
};
var textMap = {
portWarningContent: 'portWarningContent',
portSarcasmContent: 'portSarcasmContent',
discordWebhook: 'discordWebhook',
discordMention: 'discordMention',
portUpdateUrl: 'updateUrl'
};
if (cbMap[el.id]) {
AutoPort[cbMap[el.id]] = !!el.checked;
} else if (textMap[el.id]) {
AutoPort[textMap[el.id]] = el.value;
} else if (el.id === 'portThresholdAttack') {
AutoPort.portThresholdAttack = t.normalizeNumberInput('portThresholdAttack', 0, 10000, 999999999);
} else if (el.id === 'portThresholdScout') {
AutoPort.portThresholdScout = t.normalizeNumberInput('portThresholdScout', 0, 10, 999999999);
} else if (el.id === 'portRecheck') {
AutoPort.portRecheck = t.normalizeNumberInput('portRecheck', 2, 10, 300);
restartAutoPortLoop();
} else if (el.id.indexOf('portCityAttack') === 0) {
AutoPort.portCitiesAttack[parseInt(el.id.replace('portCityAttack', ''), 10)] = !!el.checked;
} else if (el.id.indexOf('portCityScout') === 0) {
AutoPort.portCitiesScout[parseInt(el.id.replace('portCityScout', ''), 10)] = !!el.checked;
} else {
return;
}
SaveAutoPortData();
t.refreshState();
},
handleClick: function (el) {
if (!el || !el.id) return;
var t = Tabs.MainTab;
var i;
if (el.id === 'portClearAll') {
for (i = 0; i < getCitySlotCount(); i++) {
AutoPort.portCitiesAttack[i] = false;
AutoPort.portCitiesScout[i] = false;
}
t.setCheckedByClass('portCityAttack', false);
t.setCheckedByClass('portCityScout', false);
} else if (el.id === 'portClearAttacks') {
for (i = 0; i < getCitySlotCount(); i++) AutoPort.portCitiesAttack[i] = false;
t.setCheckedByClass('portCityAttack', false);
} else if (el.id === 'portClearScouts') {
for (i = 0; i < getCitySlotCount(); i++) AutoPort.portCitiesScout[i] = false;
t.setCheckedByClass('portCityScout', false);
} else if (el.id === 'portAllAttacks') {
for (i = 0; i < getCitySlotCount(); i++) AutoPort.portCitiesAttack[i] = true;
t.setCheckedByClass('portCityAttack', true);
} else if (el.id === 'portAllScouts') {
for (i = 0; i < getCitySlotCount(); i++) AutoPort.portCitiesScout[i] = true;
t.setCheckedByClass('portCityScout', true);
} else if (el.id === 'discordTest') {
AutoPort.discordWebhook = document.getElementById('discordWebhook').value;
AutoPort.discordMention = document.getElementById('discordMention').value;
AutoPort.discordEnabled = document.getElementById('discordEnabled').checked;
SaveAutoPortData(true);
testDiscordWebhook();
return;
} else if (el.id === 'discordVideoGuide') {
openDiscordWebhookVideoGuide();
return;
} else if (el.id === 'portManualScan') {
checkAutoPort();
var scanStatus = document.getElementById('discordTestStatus');
if (scanStatus) scanStatus.innerHTML = 'Manual scan complete - check Detection Status';
t.refreshState();
return;
} else if (el.id === 'portSaveNow') {
SaveAutoPortData(true);
var saveStatus = document.getElementById('discordTestStatus');
if (saveStatus) saveStatus.innerHTML = 'Saved';
t.refreshState();
return;
} else if (el.id === 'portCheckUpdate') {
checkPortCoreUpdate();
return;
} else if (el.id === 'portOpenUpdate') {
openPortCoreUpdateUrl();
return;
} else {
return;
}
SaveAutoPortData();
t.refreshState();
}
};
/***********************************************************************/
function matTypeof(v) {
if (v === null) return 'null';
if (Array.isArray(v)) return 'array';
return typeof v;
}
function unixTime () {
return Math.floor(Date.now() / 1000) + unsafeWindow.g_timeoff;
}
let myServerId = null; //example: https://www150.kingdomsofcamelot.com
function getServerId() {
if (myServerId == null){
var m=/^[a-zA-Z]+([0-9]+)\./.exec(document.location.hostname);
if (m)
myServerId = m[1];
else
myServerId = '??';
}
return myServerId;
}
function LoadAutoPortData() {
var serverID = getServerId();
var s = PortCoreGetValue('PortCore_' + serverID, null);
if (s != null) {
try {
var opts = JSON2.parse(s);
for (var k in opts) {
if (!hasOwn(opts, k)) continue;
if (k === 'portPassword') continue; // removed unused/dead setting
if (AutoPort[k] == null) continue;
if (matTypeof(opts[k]) == 'object' && matTypeof(AutoPort[k]) == 'object') {
for (var kk in opts[k]) {
if (hasOwn(opts[k], kk)) AutoPort[k][kk] = opts[k][kk];
}
} else {
AutoPort[k] = opts[k];
}
}
} catch (e) {
if (DEBUG_TRACE) logit('LoadAutoPortData parse error: ' + e);
}
}
}
let PortCoreSaveTimer = null;
function SaveAutoPortData(immediate) {
var serverID = getServerId();
var doSave = function () {
PortCoreSaveTimer = null;
PortCoreSetValue('PortCore_' + serverID, JSON2.stringify(AutoPort));
};
if (PortCoreSaveTimer) clearTimeout(PortCoreSaveTimer);
if (immediate === true) doSave();
else PortCoreSaveTimer = setTimeout(doSave, 250);
}
PortCoreScriptStartup();