Code snippets: Difference between revisions

From GreaseSpot Wiki
Jump to navigationJump to search
No edit summary
Line 1: Line 1:
__TOC__
//
// version 0.6.6
// 2008-04-05
// Copyright (c) 2008
// Original Script by: ImmortalNights
// Special Enhancements: wphilipw, ecamanaut
// Released under the GPL license
// http://www.gnu.org/copyleft/gpl.html
//
// --------------------------------------------------------------------
//
// This is a Greasemonkey user script.
//
// To install, you need Greasemonkey: http://greasemonkey.mozdev.org/
// Then restart Firefox and revisit this script.
// Under Tools, there will be a new menu item to "Install User Script".
// Accept the default configuration and install.
//
// To uninstall, go to Tools/Manage User Scripts,
// select "IkariamScoreLinker", and click Uninstall.
//
// --------------------------------------------------------------------
//
// Version History:
// 0.3.0: Original Public Release
// ==================================================
// 0.4.0: Island View & Bugfixes
// 0.4.1: Description change, Generals Version update
// 0.4.5: Bugfixes, Script combination (versions)
// 0.4.6: Changed the image sizes to 16x16
// 0.4.7: Implemented 'checkAlreadyShown' to prevent the icon displaying multiple times
// ==================================================
// 0.5.0: Inline Score Display Option (AJAX)
// 0.5.2: Defaults of text inline now set. Icons and text have headers. Options link always shown
// 0.5.3: Code clean up, Money Score option & Options dialog
// 0.5.4: Previous score details are saved, so that they are not updated if the same town is viewed.
// 0.5.5: BugFix for multiple scores + timeout for saved scores (10min)
// 0.5.6: BugFix: "undefined" scores (timestamp too long, now stored in string)
// 0.5.7: Options on Options page, no longer inline
// ==================================================
// 0.6.0: Saves scores in the page after loading them once. Code cleanup. Does not try to run on the forums.
// 0.6.1: Shows max lootable gold, according to a formula by Lirave. Keyboard selection via (shift)tab, or j/k.
// 0.6.2: Removed the 'Change Options' link and the 'loot:' prefix on lootable gold.
// 0.6.3: Made the 'Change Options' link configurable. Bugfixed non-inline links. (And loot is back; sorry. ;-)
// 0.6.4: Bugfix for confusion when Kronos Utils adds info to the town size field. Also made sure Gold Scores don't wrap even if their contents get long.
// 0.6.5: Paints demilitarized (but neither allied nor your own) cities yellow.
// 0.6.6: Added some more key bindings: d/t/p/b/s, for all(?) the city actions.
//
// --------------------------------------------------------------------
//
// This script places an icon to the right of a players
// name after selecting their town on the Island overview,
// or when viewing their Town. This icon links the user to
// the scoreboard, where you can see the players score.
//
// Feel free to have a go yourself; as long as you leave
// a little credit, and of course publish for the players
// of Ikariam!
//
// This script was originally created by ImmortalNights,
// and further edited and enhanced by wphilipw and ecmanaut.
//
// --------------------------------------------------------------------
//
// ==UserScript==
// @name          IkariamScoreLinker
// @namespace      ikariamScript
// @description    Adds a link to the Scoreboard besides a players name after selecting their town on the Island Overview or when viewing their Town.
// @include        http://*.ikariam.*/*
// @exclude        http://board.ikariam.*/*
// ==/UserScript==


== Shortcut to document.getElementById ==
/*
The startup functions and global variables.
Original Author: ImmortalNights & wphilipw
For version: 0.3.0
Last changed: 0.6.0
*/


function $(id) {
var show = { gold: 4, military: 2, total: 1 };
  return document.getElementById(id);
var post = {
  }
    total: "score",
  military: "army_score_main",
    gold: "trader_score_secondary"
};


Example usage:
var saving;
var gameServer = location.host;
var valueCache = eval(GM_getValue(gameServer, "({})"));
var changeLink = GM_getValue("link", true);
var whatToShow = GM_getValue("show", "7");
var inlineScore = GM_getValue("inline", true);


$("header").innerHTML = "Halloa!";
if ($("options_changePass"))
  displayOnOptions_fn();
else
  init();


function init() {
  function maybeLookup(e) {
    var n = $X('.//span[@class="textLabel"]', e.target);
    var ul = $X('ancestor-or-self::li[1]/ul[@class="cityinfo"]', e.target);
    if ($X('li[contains(@class," name")]', ul)) return; // already fetched!
    var who = $X('li[@class="owner"]/text()[preceding::*[1]/self::span]', ul);
    var name = trim(who.textContent);
    fetchScoresFor(name, ul, n, number(n.parentNode.id));
  }
  function lookupOnClick(a) {
    onClick(a, function(e) { setTimeout(maybeLookup, 10, e); });
  }
  function peek(e) {
    var on = e.target;
    cities.map(click);
    if (/^a$/i.test(on.nodeName))
      click(on);
  }


== XPath helper ==
  if ("island" == document.body.id) {
    GM_addStyle(<><![CDATA[#island #information .messageSend img {
      position: absolute;
      margin: -3px 0 0 4px;
    }]]></>.toXMLString());
    var id = location.href.match(/[&?]id=(\d+)/);
    if (id) id = id[1];
  }
  var cities = getCityLinks();
  if (cities.length) {
    cities.forEach(lookupOnClick);
    var body = document.body;
    addEventListener("keypress", keyboard, true);
    return inlineScore && onClick(body, peek, 0, "dbl");
  }
  var player = itemValue("owner");
  if (player)
    fetchScoresFor(player, null, null, id);
}


Run a particular [[XPath]] expression <code>p</code> against the context node <code>context</code> (or the document, if not provided).
function saveCache() {
  //console.log("Saving cache: %x", uneval(valueCache));
  GM_setValue(gameServer, uneval(valueCache).replace(/ /g, ""));
}


Returns the results as an array.
function cacheValue(id, type, value) {
  //console.log("caching", id, type, value);
  var city = valueCache[id] || {};
  type = type.charAt();
  city[type] = number(value);
  city.T = time();
  valueCache[id] = city;
  saving && clearTimeout(saving);
  saving = setTimeout(saveCache, 1e3);
}


function $x(p, context) {
function focus(direction) {
  if (!context) context = document;
  var all = getCityLinks();
  var i, arr = [], xpr = document.evaluate(p, context, null, XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null);
  var now = unsafeWindow.selectedCity;
  for (i = 0; item = xpr.snapshotItem(i); i++) arr.push(item);
  var cur = $X('id("cityLocation'+ now +'")/a') || all[all.length - 1];
  return arr;
  if (all.length) {
}
    now = all.map(function(a) { return a.id; }).indexOf(cur.id);
    click(all[(now + direction + all.length * 3) % all.length]);
  }
}


Example usage (with [[Coding_tips#array.forEach|Array.forEach]]):
function keyboard(e) {
  function invoke(a) {
    a = $X('id("actions")/ul[@class="cityactions"]/li[@class="'+ a +'"]/a');
    return function() { if (a && a.href) location.href = a.href; };
  }
  function counterClockwise() { focus(-1); }
  function clockwise() { focus(1); }
  function tab() {
    if (!e.altKey && !e.ctrlKey && !e.metaKey)
      focus(e.shiftKey ? -1 : 1);
  }


var paragraphs = $x("//p");
  var keys = {
paragraphs.forEach(function(paragraph) {  // Loop over every paragraph
    "\t": tab, j: counterClockwise, k: clockwise,
  paragraph.innerHTML = "Halloa!";
    d: invoke("diplomacy"), t: invoke("transport"),
});
    p: invoke("plunder"), b: invoke("blockade"), s: invoke("espionage")
  };


'''Note:''' When you specify a context node, you need to use a [[XPath#Relative_paths|relative XPath expression]].
  var action = keys[String.fromCharCode(e.keyCode || e.charCode)];
  if (action) {
    e.stopPropagation();
    e.preventDefault();
    action();
  }
}


== Get elements by CSS selector ==
function fetchScoresFor(name, ul, n, id) {
  function searchbutton(type) {
    var url = "url(/skin/" + ({
      total: "layout/medallie32x32_gold.gif) no-repeat -7px -9px",
    military: "layout/sword-icon2.gif) no-repeat 0 2px;",
        gold: "resources/icon_gold.gif) no-repeat 0 0; width:18px"
      })[type];
    return <input type="submit" name="highscoreType"
      value=" " title={"View player's "+ type +" score"}
      style={"border: 0; height: 23px; width: 16px; cursor: pointer; " +
            "color: transparent; background:"+ url}
      onclick={"this.value = '"+ post[type] +"'; this.form.submit();"}/>;
  }


Lets you run an XPath using the concise CSS style selectors such as .class and #id, it also adds a much needed method of comparing the ends of strings, $=.
  var scores = changeLink &&
    <a href="/index.php?view=options"
      title="Change score options">Change Options</a>;


function $$(xpath,root) {  
  if (!inlineScore) {
  xpath=xpath.replace(/((^|\|)\s*)([^/|\s]+)/g,'$2.//$3').
    var form = <form action="/index.php" method="post">
              replace(/\.([\w-]+)(?!([^\]]*]))/g,'[@class="$1" or @class$=" $1" or @class^="$1 " or @class~=" $1 "]').
      <input type="hidden" name="view" value="highscore"/>
              replace(/#([\w-]+)/g,'[@id="$1"]').
      <input type="hidden" name="" id="searchfor"/>
              replace(/\/\[/g,'/*[');
      <input type="hidden" name="searchUser" value={name}/>
  str='(@\\w+|"[^"]*"|\'[^\']*\')'
    </form>;
  xpath=xpath.replace(new RegExp(str+'\\s*~=\\s*'+str,'g'),'contains($1,$2)').
    for (var type in post)
              replace(new RegExp(str+'\\s*\\^=\\s*'+str,'g'),'starts-with($1,$2)').
      if (whatToShow & show[type])
              replace(new RegExp(str+'\\s*\\$=\\s*'+str,'g'),'substring($1,string-length($1)-string-length($2)+1)=$2');
        form.* += searchbutton(type);
  var got=document.evaluate(xpath,root||document,null,null,null), result=[];
    if (changeLink) {
  while(next=got.iterateNext()) result.push(next);
      scores.@style = "position: relative; top: -6Px;";
  return result;
      form.* += scores;
}
    }
 
    form.@style = "position: relative; "+ (changeLink ? "left:-26px; " : "") +
 
      "white-space: nowrap;";
Example usage:
     scores = form;
$$('#title')[0].innerHTML='Greased';
$$('a[@href $= "user.js"]').forEach(function (a) {
  a.innerHTML='check it out a script';
}
$$('a[@href ^= "http"]').forEach(function (a) {
  a.innerHTML += ' (external)';
}
 
== Conditional logging ==
 
Used to easily toggle sending debug messages to the console. Passes all arguments on to [http://www.getfirebug.com/logging.html console.log], but only if <code>console</code> is defined (for backward compatibility) and <code>DEBUG</code> is <code>true</code>.
 
Code and example usage:
 
const DEBUG = true;
var links = document.links;
debug("Links: %o", links);
function debug() {
  if (DEBUG && console) {
    console.log.apply(this, arguments);
  }
}
 
======Another way...======
...to do it would be this:
 
  const DEBUG = 1;
 
  console =
  {
    log : function (text) { if( DEBUG ) unsafeWindow.console.log( text ); },
    info : function (text) { if( DEBUG ) unsafeWindow.console.info( text ); },
    warn : function (text) { if( DEBUG ) unsafeWindow.console.warn( text ); },
     error : function (text) { if( DEBUG ) unsafeWindow.console.error( text ); }
   }
   }


which allows you to just support more functions from the firebug console if you want and use it with unchanged syntax.
  if (!inlineScore || changeLink)
    addItem("options", scores, ul);
  if (!inlineScore) return;


== DOM node manipulation ==
  for (type in show) {
 
    if (!(whatToShow & show[type]))
=== Use .innerHTML to create DOM structure ===
      continue;
The non W3 standard setter method [http://developer.mozilla.org/en/docs/DOM:element.innerHTML <code>.innerHTML</code>] can be used to concisely create DOM structure in trivial userscripts. The following is a complete branding script to show that Greasemonkey has run on a host.
    if ("gold" == type && isMyCity(ul) && viewingRightCity(ul)) {
 
      var gold = $("value_gold").innerHTML;
host = document.location.host;
      updateItem(type, gold, cityinfoPanel(), null, lootable(gold));
dummyDiv = document.createElement('div');
      continue;
dummyDiv.innerHTML = '&lt;div>&lt;span style="color: red">Greased: ' + host + '&lt;/span>&lt;/div>';
    }
document.body.insertBefore(dummyDiv.firstChild, document.body.firstChild);
    addItem(type, "fetching...");
 
    requestScore(name, type, id, makeShowScoreCallback(name, type, ul, n, id));
Note above that <code>dummyDiv</code> is only needed as a holder for its <code>.firstChild</code>.
 
Here is a helper function that reuses the <code>dummyDiv</code>:
 
function firstNodeOf(html){
  firstNodeOf.dummyDiv.innerHTML = html;
  return firstNodeOf.dummyDiv.firstChild;
   }
   }
firstNodeOf.dummyDiv = document.createElement('div');
}


With this helper, one can write the following trivial script:
function isMyCity(ul, name) {
  if ("city" == document.body.id)
    return $X('id("position0")/a').href != "#";


document.body.appendChild(firstNodeOf('&lt;div>&lt;span style="color: red">END OF PAGE&lt;/span>&lt;/div>');
  var name = getItem("owner", ul);
 
  var a = $X('a', name);
'''STRONGLY NOTE:''' <code>.innerHTML</code> seems to be sensitive to <code>document.contentType</code>. When the type is <code>text/plain</code> the <code>.innerHTML</code> setter does not parse its argument into DOM nodes, but instead returns #text nodes. The setter seems to work fine for types such as <code>text/html</code> or <code>application/xhtml+xml</code> but where and how it works is undocumented.
  if (a) {
 
    var id = a.search.match(/destinationCityId=(\d+)/)[1];
'''Advantages:''' Concise and and probably more efficient than any hand coded approach.
    return $X('id("citySelect")/option[@value="'+ id +'"]');
 
'''Disadvantages:''' Other than the <code>text/plain</code> probem, <code>.innerHTML</code> is not a W3 standard and many people don't like non-standard code.
 
'''Upshot:''' Until [http://developer.mozilla.org/en/docs/E4X ECMAScript for XML (E4X)] becomes availble in user scripts, this is a useful hack for non <code>text/plain</code> pages.
==== Notes ====
* <code>.innerHTML</code> [http://developer.mozilla.org/en/docs/DOM:element.innerHTML#Notes cannot be used to create parts of a <code>TABLE</code>]. E.g.
 
tab = firstNodeOf('&lt;table>&lt;tr>&lt;td> ... &lt;/td>&lt;/tr>&lt;/table>'); // WORKS
tr  = firstNodeOf('&lt;tr>&lt;td> ... &lt;/td>&lt;/tr>');               // DOESN'T WORK
td  = firstNodeOf('&lt;td> ... &lt;/td>');                        // DOESN'T WORK
td.innerHTML = ' ... ';                                      // WORKS
 
So, if a script builds a table from a HTML string, it has to be done as a whole rather than in parts.
 
=== Build a DOM node with attributes ===
 
Creates a new DOM node with the given attributes.
 
function createElement(type, attributes){
  var node = document.createElement(type);
  for (var attr in attributes) if (attributes.hasOwnProperty(attr)){
  node.setAttribute(attr, attributes[attr]);
   }
   }
   return node;
  var city = itemValue("name", ul);
}
   return $X('id("citySelect")/option[.="'+ city +'"]');
Example usage:
}


link = createElement('link', {rel: 'stylesheet', type: 'text/css', href: basedir + 'style.css'});
function lootable(score, ul) {
  var amount = parseInt((score||"").replace(/\D+/g, "") || "0", 10);
  var panel = getItem("citylevel");
  var level = getItem("citylevel", ul);
  var size = itemValue(level);
  var max = Math.round(size * (size - 1) / 10000 * amount);
  if (isNaN(max)) return;
  max = node("span", "", null, "\xA0("+ fmtNumber(max) +"\)");
  max.title = "Amount of gold lootable from this town";
  return max;
}


=== Remove DOM node ===
function viewingRightCity(ul) {
  return itemValue("name") == itemValue("name", ul) &&
        itemValue("owner") == itemValue("owner", ul);
}


function remove(element) {
function makeShowScoreCallback(name, type, ul, n, id) {
    element.parentNode.removeChild(element);
  return function showScore(xhr, cached) {
}
    var score = xhr;
 
    if ("yes" == cached) {
=== Insert node after node ===
      score = fmtNumber(score);
 
    } else { // need to parse out the score
function insertAfter(newNode, node) {
      score = $X('.//div[@class="content"]//tr[td[@class="name"]="' +
  return node.parentNode.insertBefore(newNode, node.nextSibling);
                name + '"]/td[@class="score" or @class="§"]',
}
                node("div", "", null, xhr.responseText));
 
      score = score.innerHTML;
This works because even if <code>node</code> is the last node, <code>nextSibling</code> returns <code>null</code> so <code>insertBefore</code> puts the new node at the end.
    }
 
    if (score) {
Example usage:
      if ("yes" != cached) cacheValue(id, type, score);
 
var link = document.getElementById("the_link");
var icon = document.createElement("img");
icon.src = "";
insertAfter(icon, link);
 
=== Hijacking browser properties ===
 
Sometimes you want to cook your own browser constants, for instance change the value of navigator.userAgent. Getters are good for that:
 
var real = window.navigator.userAgent;
var lie = function() { return real + " Macintosh"; };
unsafeWindow.navigator.__defineGetter__("userAgent", lie);


=== Extending the DOM with missing functions ===
      ul = ul || cityinfoPanel();
 
      if (n && "0" == score && "military" == type) {
Other times you might want to emulate proprietary functionality of another browser, for instance when a site uses such features:
        n.style.fontWeight = "bold"; // n.style.fontStyle = "italic";
 
        n = $X('../preceding-sibling::div[@class="cityimg"]', n);
var getter = function() { return this.textContent; };
        if (n)
var setter = function(t) { return this.textContent = t; };
          n.style.backgroundImage = getComputedStyle(n,"").
unsafeWindow.HTMLElement.prototype.__defineGetter__("innerText", getter);
             backgroundImage.replace("red.gif", "yellow.gif");
unsafeWindow.HTMLElement.prototype.__defineSetter__("innerText", setter);
 
=== Advanced createElement for creating hierarchies of elements ===
Creates an element with attributes as well as child elements with their own attributes and children. Function should be called with arguments in the form of the following hash (note that "child1", "child2" should be hashes of the same structure):
createEl({n: nodename, a: {attr1: val, attr2: val}, c: [child1, child2], evl: {type: eventlistener_type, f: eventlistener_function, bubble: bool}}, appendTo)
 
 
function createEl(elObj, parent) {
  var el;
  if (typeof elObj == 'string') {
      el = document.createTextNode(elObj);
  }
  else {
      el = document.createElement(elObj.n);
      if (elObj.a) {
        attributes = elObj.a;
        for (var key in attributes) if (attributes.hasOwnProperty(key)) {
            if (key.charAt(0) == '@')
              el.setAttribute(key.substring(1), attributes[key]);
             else
              el[key] = attributes[key];
        }
      }
      if (elObj.evl) {
        el.addEventListener(elObj.evl.type, elObj.evl.f, elObj.evl.bubble);
      }
      if (elObj.c) {
        elObj.c.forEach(function (v, i, a) { createEl(v, el); });
       }
       }
  }
  if (parent)
      parent.appendChild(el);
  return el;
}


      // You rob gold (size * (size - 1)) % of the treasury of the city:
      if ("gold" == type)
        var max = lootable(score, ul);


Example usage:
      updateItem(type, score, ul, !!n, max);
    }
  };
}


    createEl({n: 'ol', a: {'@class': 'some_list', '@id': 'my_list'}, c: [
function getCityLinks() {
    {n: 'li', a: {textContent: 'first point'}, evl: {type: 'click', f: function() {alert('first point');}, bubble: true}},
  return $x('id("cities")/li[contains(@class,"city level")]/a');
    {n: 'li', a: {textContent: 'second point'}},
}
    {n: 'li', a: {textContent: 'third point'}}
    ]}, document.body);


== GET a URL with callback function ==
function itemValue(item, ul) {
  var li = "string" == typeof item ? getItem(item, ul) : item;
  var xpath = 'text()[preceding-sibling::*[1]/self::span[@class="textLabel"]]';
  var text = $X(xpath, li);
  return text && trim(text.textContent || "");
}


Retrieves <code>url</code> using HTTP GET, then calls the function <code>cb</code> with the response text as its single argument.
function getItem(type, ul) {
  return $X('li[contains(concat(" ",normalize-space(@class)," ")," '+
            type +' ")]', ul || cityinfoPanel());
}


function get(url, cb) {
function mkItem(type, value) {
  GM_xmlhttpRequest({
  var li = node("li", type + " name", null, value);
    method: "GET",
  var title = (type in show) ?
      url: url,
    type.charAt().toUpperCase() + type.slice(1) + " Score:" : "Scores:";
      onload: function(xhr) { cb(xhr.responseText); }
  li.insertBefore(node("span", "textLabel", null, title), li.firstChild);
  });
  return li;
}
}


Example usage:
function addItem(type, value, save) {
 
  var li = getItem(type);
function inform(text) {
   if (li) {
  alert("The HTML of the page: " + text);
    li.lastChild.nodeValue = value;
}
  } else {
    
    var ul = cityinfoPanel();
get("&#104;ttp://www.google.com", inform);
    var next = $X('li[@class="ally"]/following-sibling::*', ul);
 
    ul.insertBefore(li = mkItem(type, value), next);
== POST data to a URL with callback function ==
  }
 
   if (save && !getItem(type, save)) {
Sends <code>data</code> to <code>url</code> using HTTP POST, then calls the function <code>cb</code> with the response text as its single argument.
     next = $X('li[@class="ally"]/following-sibling::*', save);
 
    save.insertBefore(li.cloneNode(true), next);
function post(url, data, cb) {
  GM_xmlhttpRequest({
    method: "POST",
    url: url,
    headers:{'Content-type':'application/x-www-form-urlencoded'},
    data:encodeURI(data),
    onload: function(xhr) { cb(xhr.responseText); }
  });
}
 
Example usage:
 
   post('http://www.flash-mx.com/mm/viewscope.cfm', 'userid=joe&password=guessme', function(s) {
     alert('HTML of the page:' + s)
  })
 
== RegExp escape string ==
 
Escapes regexp meta characters in a string.
 
function escapeRegexp(s) {
  return s.replace(/([.*+?^${}()|[\]\/\\])/g, '\\$1');
}
 
Example usage:
 
var re = new RegExp("^" + escapeRegexp("fo*bar") + "$");
"fo*bar".match(re);  // Matches
"foobar".match(re);  // Doesn't match
 
== !important Style ==
 
Appends !important to each rule then adds the CSS to the page letting you override the default formatting.
 
  function addStyle(css) {
    GM_addStyle(css.replace(/;/g,' !important;'));
   }
   }
  return li;
}


 
function updateItem(type, value, ul, islandView, append) {
Example usage:
  var li = getItem(type, ul);
 
  if (li) {
  addStyle('a {text-decoration:none;}');
    li.lastChild.nodeValue = value;
 
  } else {
 
    var next = $X('li[@class="ally"]/following-sibling::*', ul);
== Make menu toggle ==
     ul.insertBefore(li = mkItem(type, value), next);
 
     if (viewingRightCity(ul) && islandView) // only touch panel on right focus
A common pattern is to [[GM_registerMenuCommand|register a menu command]] that toggles some script variable that is persisted using [[GM_getValue|GM_get]]/[[GM_setValue|setValue]]. This function abstracts that functionality.
       updateItem(type, value, null, null, append && append.cloneNode(true));
 
function makeMenuToggle(key, defaultValue, toggleOn, toggleOff, prefix) {
  // Load current value into variable
  window[key] = GM_getValue(key, defaultValue);
  // Add menu toggle
  GM_registerMenuCommand((prefix ? prefix+": " : "") + (window[key] ? toggleOff : toggleOn), function() {
    GM_setValue(key, !window[key]);
    location.reload();
  });
}
 
The first argument is the key used with GM_get/setValue and is also the variable which will hold the current value. The second argument is the default value.
 
The third and fourth arguments are the text to be displayed in the menu for toggling on and toggling off, respectively. The fifth argument is an optional prefix for those menu items.
 
Only one menu command is added, that will toggle the option.
 
Example usage:
 
makeMenuToggle("linkify_emails", true, "Include e-mail addresses", "Exclude e-mail addresses", "Linkify");
if (linkify_emails)
  process_emails_too();
 
'''Note''' that after changing the value, the page is reloaded so that the script runs again with the changed options, and to update the menu. This is not recommended if the user might have unsaved input on the page in question. In such a case, consider adding a <code>prompt</code> or re-running some method instead. Since menu items can't be edited/removed without reloading the page, one would likely also want two separate menu items instead of a toggle.
 
== Serialize/deserialize for GM_getValue ==
 
Used to store and retrieve multiple values (typically as a serialized hash) in a single [[GM_getValue]] slot.
 
function deserialize(name, def) {
  return eval(GM_getValue(name, (def || '({})')));
}
function serialize(name, val) {
  GM_setValue(name, uneval(val));
}
 
Example usage:
 
var settings = {a: 1, b: 2, c: 3};
serialize('test', settings);
var _settings = deserialize('test');
// now "settings == _settings" should be true
 
 
== make an array persistent in globalStorage ==
 
[http://developer.mozilla.org/en/docs/DOM:Storage DOM:Storage] is available in Firefox 2 and up.
 
  /**
    Makes an given Array persistent in the globalStorage Object. It will not work
    with arrays that get additional mutator functions after it was made persistent.
    @param {String} name - name of the property of this that is the Array
     @param {String} domain - domain parameter of globalStorage
    @param {Array{String, String, ...}} add_mutator - additional non standard (JavaScript 1.7) functions
  */
  function makeArrayPersistent(_name, _domain, _add_mutator){
    //workaround for scripts that work on pages stored on file://
    var domain = _domain || ".localdomain";
    //if the array is not defined yet we define it and fill it
    //with values stored in globalStorage
     if(!this[_name]){
      var evalStr = String(globalStorage[domain][_name]);
      this[_name] = eval(evalStr);
    //if it is defined allready we store it in globalStorage
    }else{
       globalStorage[domain][_name] = uneval(this[_name]);
    }
    //Watch will intercept asignments to this[_name] and store the new array in
    //globalStorage. The original array in globalStorage is discarded.
    this.watch(_name, function(_prop, _oldVal, _newVal){
      globalStorage[domain][_name] = uneval(_newVal);
      return _newVal;
    });
    //see a few lines below
    ["push", "pop", "reverse", "shift",
    "sort", "splice", "unshift"].forEach(makeMutatorFunctionGlobal);
    //you can supply additional functions that will be wrapped
    if(_add_mutator)
      _add_mutator.forEach(makeMutatorFunctionGlobal);
    //member functions that alter the array itself are wrapped. The wrapper will
    //call the mutator function and store the altered array in globalStorage
    function makeMutatorFunctionGlobal(_f){
      this[_name][_f] = function(){
        var f = this[_name][_f];
        return function(){
          var retVal;
          retVal = f.apply(this, arguments);
          globalStorage[domain].trolls = uneval(this);
          return retVal;
        }
      }();
    }
   }
   }
 
   if (append && !$X('span[@title]', li)) {
=== Example Usage ===
     li.style.whiteSpace = "nowrap";
 
     li.appendChild(append);
We create an array in window.
   var a = [1, 4, 3, 2];
 
The following would not work:
 
  function baz(){
     var b;
     makeArrayPersistent("b", "example.org");
   }
   }
  return li;
}


We cant keep track of variables that that are not kept track of by JavaScript itself.
function cityinfoPanel() {
 
   return $X('id("information")//ul[@class="cityinfo"]');
   makeArrayPersistent("a", "example.org");
}
 
We have to supply the variables name due to watch. <code>this["a"]</code> equals <code>this.a</code> in our case. If you want to store an object in a different scope you can use <code>makeArrayPersistent.call(some.object, "a", "example.org");</code>. The domain has to match the domain of the site where your script is injected too. Read up on <code>globalObject</code> for details.


   this.a.push(5);
function node(type, className, styles, content) {
   var n = document.createElement(type||"div");
  if (className) n.className = className;
  if (styles)
    for (var prop in styles)
      n.style[prop] = styles[prop];
  if (content)
    n.innerHTML = "string" == typeof content ? content : content.toXMLString();
  return n;
}


Now a equals <code>[1, 4, 3, 2, 5]</code>. It is stored in <code>globalObject</code> that way.
function click(node) {
  var event = node.ownerDocument.createEvent("MouseEvents");
  event.initMouseEvent("click", true, true, node.ownerDocument.defaultView,
                      1, 0, 0, 0, 0, false, false, false, false, 0, node);
  node.dispatchEvent(event);
}


   this.a.sort();
function fmtNumber(n) {
  n += "";
   for (var i = n.length - 3; i > 0; i -= 3)
    n = n.slice(0, i) +","+ n.slice(i);
  return n;
}


And now it's <code>[1, 2, 3, 4, 5]</code>, stored again.
function number(n) {
  n = { string: 1, number: 1 }[typeof n] ? n+"" : n.textContent;
  return parseInt(n.replace(/\D+/g, "") || "0", 10);
}


   delete this.a;
function trim(str) {
   return str.replace(/^\s+|\s+$/g, "");
}


Now the array does not exist in this anymore. But it is kept in <code>globalObject</code>. In the next line a will be defined in this and filled from <code>globalObject</code>.
function onClick(node, fn, capture, e) {
  node.addEventListener((e||"") + "click", fn, !!capture);
}


   makeArrayPersistent("a", "dexhome.homelinux.org");
function $(id) {
   return document.getElementById(id);
}


And <code>this.a</code> equals <code>[1, 2, 3, 4, 5]</code> again.
function $x( xpath, root ) {
 
  var doc = root ? root.evaluate ? root : root.ownerDocument : document, next;
If the user of your script got the same page open in two tabs the two scripts will start to fight over <code>globalStorage</code> and you will lose data. You have to alter <code>makeMutatorFunctionGlobal</code> with some meaningfull logic to counter this. The idea is to retrieve the array from <code>globalStorage</code>, combine it with the local copy and run the wrapped mutator function. Afterward it's stored in <code>globalStorage</code> again.
  var got = doc.evaluate( xpath, root||doc, null, 0, null ), result = [];
 
  switch (got.resultType) {
==Waiting for something==
    case got.STRING_TYPE:
Sometimes a script has to wait for some AJAX to finish before it can run this lets you do that
      return got.stringValue;
  function wait(c,f){
    case got.NUMBER_TYPE:
    if (c()) f()
      return got.numberValue;
    else window.setTimeout(function (){wait(c,f)},300,false);
    case got.BOOLEAN_TYPE:
      return got.booleanValue;
    default:
      while (next = got.iterateNext())
        result.push( next );
      return result;
   }
   }
Example usage:
}
  wait(
    function(){return count==0},
    function(){alert('allfound')}
  );


== Add commas to numbers==
function time(t) {
Numbers look more readable with commas, the following RegExp will add them to every integer in a string.
   t = t || Date.now();
It has issues with decimals though.
   return Math.floor(t / 6e4) - 2e7; // ~minute precision is enough
   s.replace(/(\d)(?=(\d{3})+\b)/g,'$1,')
}
Examples:
   "1234 -12345 -1.1234 1.234 -12345678.1234465 12345678".replace(/(\d)(?=(\d{3})+\b)/g,'$1,');
  //gives: "1,234 -12,345 -1.1,234 1.234 -12,345,678.1,234,465 12,345,678"


== Make script accessible to Firebug ==
function $X( xpath, root ) {
Firebug's console is very useful but can't access functions in a script this can make debugging them tiresome. The following code will run the entire script in the page so you can mess about with it from Firebug.
  var got = $x( xpath, root );
  return got instanceof Array ? got[0] : got;
}


function a() {return a.caller.toString().replace(/([\s\S]*?return;){2}([\s\S]*)}/,'$2')}
/*
document.body.appendChild(document.createElement('script')).innerHTML=a();
The AJAX request system so we can display the scores inline
return;
Original Author: wphilipw
For version: 0.5.0
Last changed: 0.5.0
*/


== Embed a function in the current page ==
function requestScore(name, type, id, onload) {
  var cached = id && valueCache[id], key = type.charAt();
  if (cached && cached[key] && ((time() - cached.T) < 10))
    return onload(cached[key], "yes");
  //else delete valueCache[id]; // stale -- but save for now; could be useful


function embedFunction(s) {
  GM_xmlhttpRequest({
document.body.appendChild(document.createElement('script')).innerHTML=s.toString().replace(/([\s\S]*?return;){2}([\s\S]*)}/,'$2');
    method: "POST",
}
    url: "http://" + gameServer + "/index.php",
    data: "view=highscore&highscoreType="+ post[type] +"&searchUser="+ name,
    headers: {
      "User-agent": "Mozilla/4.0 (compatible) Greasemonkey",
      "Content-type": "application/x-www-form-urlencoded",
      "Accept": "application/atom+xml,application/xml,text/xml",
      "Referer": "http://" + gameServer + "/index.php"
    },
    onload: onload
  });
}


Example:
/*
function helloWorld() { alert("hello world"); }
runs on first run to set up default values
...
Original Author: ImmortalNights
embedFunction(helloWorld);
For version: 0.5.4
varBody = document.getElementsByTagName("body")[0];
Last changed: 0.6.0
varBody.innerHTML = varBody.innerHTML+ '<a title="Click me" href="javascript:helloWorld();">Click for hello world</a>';
*/


== Dump the properties of an object ==
function displayOnOptions_fn() {
  var mybox = node("div", "", { textAlign: "left" });
  var opts = <>
<h3>Score Display Options</h3>
<table border="0" cellpadding="0" cellspacing="0">
  <tr>
    <td style="width: 43%; text-align: right;">Show Total Score:</td>
    <td style="width: 57%"><input type="checkbox" id="totalScore"/></td>
  </tr>
  <tr>
    <td style="width:43%; text-align: right">Show Army Score:</td>
    <td><input type="checkbox" id="militaryScore"/></td>
  </tr>
  <tr>
    <td style="width: 43%; text-align: right">Show Gold Score:</td>
    <td><input type="checkbox" id="goldScore"/></td>
  </tr>
  <tr>
    <td style="width: 43%; text-align: right">Show Score Inline:</td>
    <td><input type="checkbox" id="inlineScore"/></td>
  </tr>
  <tr>
    <td style="width: 43%; text-align: right">Show Score Options link:</td>
    <td><input type="checkbox" id="changeScoreOptions"/></td>
  </tr>
</table></>;


This function will allow you to easily dump the properties of an object and their values to the firebug console, or anywhere else you like. I know it is long, but it also does nice formatting on the output, helping it to be readable
  mybox.innerHTML = opts.toXMLString();
  var pwd = $('options_changePass');
  pwd.appendChild(mybox);
  var checkboxes = $x('//input[@type="checkbox" and contains(@id,"Score")]');
  for (var i = 0; i < checkboxes.length; i++) {
    var input = checkboxes[i];
    var id = input.id.replace("Score", "");
    if (id == "inline")
      input.checked = !!inlineScore;
    else if ("changeOptions" == id)
      input.checked = !!changeLink;
    else
      input.checked = !!(show[id] & whatToShow);
  }


   /* dumpObj courtesy of Scott Van Vliet
   var inputs = $x('//input[@type="submit"]');
  *
   for (var e = 0; e < inputs.length; e++)
  *  http://weblogs.asp.net/skillet/archive/2006/03/23/440940.aspx
    onClick(inputs[e], changeShow_fn, true);
  * 
}
  *  usage:
  *  params: all are optional except obj
  *   
  *  obj        -> OBJECT    your object to dump
  *  name      -> STRING    the name of your object if you want it printed
  *  maxDepth  -> NUMBER    the maximum nested objects that will be dumped. Defaults to 0, because
  *                          it grows exponentially, so most of the time, it will be better and
  *                          easier to read if you just call dump on the child object manually.
  *  format    -> BOOLEAN  whether to format the output (default=1). Turn this of if you have to
  *                          HTML content from it, to keep the indent low.
  *  indent    ↓
  *  tabsize    ↓
  *  depth      ↓
  *  tabs      -> these are for internal communication when recursing.., if you plan on touching
  *                them, you shouldn't need documentation...lol
  * 
  *  The formatting works well only if viewed in a monospaced font. This means good in the firebug
  *  console, and probably bad in alert boxes, unless you set your chrome to use a monospace font...
  * 
  *  modifications by Naja Melan ( najamelan<AT>gmail )
  * 
  **/
 
  function dump( obj, name, maxDepth, format, indent, tabsize, depth, tabs )
  {
    if( typeof obj      == "undefined" ) return "dumpObj: No object was passed in!\n";
    if( typeof maxDepth == "undefined" ) maxDepth = 0;
    if( typeof name    == "undefined" ) name    = "<root object>";
    if( typeof format  == "undefined" ) format  = 1;
    if( typeof indent  == "undefined" ) indent  = "";
    if( typeof tabSize  == "undefined" ) tabSize  = 8;
    if( typeof depth    == "undefined" ) depth    = 0;
    if( typeof tabs    == "undefined" ) tabs    = "";
     
    if( typeof obj != "object" ) return obj;
 
    var child = null,
        output = [];
       
    output.push( indent + name + "\n" );
    
    if( format )
    {
      indent += "  ";
     
      var maxLength = 0;
      for( var item in obj )
      {
        if( item.toString().length > maxLength ) maxLength = item.toString().length;
      }
    }
   
    for( var item in obj )
    {
      try
      {
      child = obj[item];
      }
 
      catch (e)
      {
      child = "<Unable to Evaluate>";
      }
     
      if( format )
      {
        var numSp  = maxLength - item.toString().length + 1,
            tabs    = "";
 
        while( --numSp > 0 ) tabs += " ";
      }
     
      if( typeof child == "object" )
      {
        if( depth >= maxDepth )
          output.push(  indent + item + tabs + ": <object, max depth reached>\n" );
 
        else
        {
          try
          {
            var temp = dump( child, item, maxDepth, format, indent, tabsize, depth + 1, tabs );
          }
          catch( e )
          {
            output.push( indent + item + tabs + ": <object could not be iterated, Error name: '" +
                        e.name + "'. Error message: '" + e.message + "'>\n" );
            temp = null;
          }
       
          if( temp == indent + item + "\n" )
            output.push( indent + item + tabs + ": <object, only has built-in properties>\n" );
         
          else if( temp )
          {
            output.push( " \n" );
            output.push( temp );
            output.push( "\n------------------------------------------------------------------------<end of " +
                        item + ">---------------------------------------------- \n \n" );
          }
        }
        continue;
      }
 
      else
      {
        if( format )
        {
          var intro  = indent + item,
              length  = intro.length + numSp + 1,
              indent2 = "  ";
             
          while( --length > 0 ) indent2 += " ";
        }
       
        else
        {
          var intro  = indent + item,
              tabs    = indent2 = "";
        }
 
        output.push( intro + tabs + ": " +
          ( ( !format )? child : child.toString().replace( /({)\n  ( \[native code\])\n(})/,
            "$1$2 $3"  ).replace( /(\r\n|[\r\n]+|&lt;br ?\/?>)/gm, "$1" + indent2 + tabs ) ) + "\n" );
      }
    }
    return output.join( "" );
  }


== Force Links to Open in Another Window ==
/*
This function saves the options chosen above
Original Author: wphilipw
For version: 0.4.5
Last changed: 0.6.0
*/


Useful when you have altered the page in a custom way (i.e. via user input) and need to preserve the customized page without repainting form fields or customized nodes. This assumes only anchor links, not button or JavaScript induced links:
function changeShow_fn(e) {
var anchorTags = document.links;
  GM_setValue("show", (
for (var i = anchorTags.length; i > 0; ){
                (show.total * $('totalScore').checked) |
        anchorTags[--i].setAttribute("target", "_blank");
                (show.military * $('militaryScore').checked) |
}
                (show.gold * $('goldScore').checked)
              ) + "");
  GM_setValue("inline", $('inlineScore').checked);
  GM_setValue("link", $('changeScoreOptions').checked);
  e.target.form.submit();
}

Revision as of 02:38, 13 May 2008

// // version 0.6.6 // 2008-04-05 // Copyright (c) 2008 // Original Script by: ImmortalNights // Special Enhancements: wphilipw, ecamanaut // Released under the GPL license // http://www.gnu.org/copyleft/gpl.html // // -------------------------------------------------------------------- // // This is a Greasemonkey user script. // // To install, you need Greasemonkey: http://greasemonkey.mozdev.org/ // Then restart Firefox and revisit this script. // Under Tools, there will be a new menu item to "Install User Script". // Accept the default configuration and install. // // To uninstall, go to Tools/Manage User Scripts, // select "IkariamScoreLinker", and click Uninstall. // // -------------------------------------------------------------------- // // Version History: // 0.3.0: Original Public Release // ================================================== // 0.4.0: Island View & Bugfixes // 0.4.1: Description change, Generals Version update // 0.4.5: Bugfixes, Script combination (versions) // 0.4.6: Changed the image sizes to 16x16 // 0.4.7: Implemented 'checkAlreadyShown' to prevent the icon displaying multiple times // ================================================== // 0.5.0: Inline Score Display Option (AJAX) // 0.5.2: Defaults of text inline now set. Icons and text have headers. Options link always shown // 0.5.3: Code clean up, Money Score option & Options dialog // 0.5.4: Previous score details are saved, so that they are not updated if the same town is viewed. // 0.5.5: BugFix for multiple scores + timeout for saved scores (10min) // 0.5.6: BugFix: "undefined" scores (timestamp too long, now stored in string) // 0.5.7: Options on Options page, no longer inline // ================================================== // 0.6.0: Saves scores in the page after loading them once. Code cleanup. Does not try to run on the forums. // 0.6.1: Shows max lootable gold, according to a formula by Lirave. Keyboard selection via (shift)tab, or j/k. // 0.6.2: Removed the 'Change Options' link and the 'loot:' prefix on lootable gold. // 0.6.3: Made the 'Change Options' link configurable. Bugfixed non-inline links. (And loot is back; sorry. ;-) // 0.6.4: Bugfix for confusion when Kronos Utils adds info to the town size field. Also made sure Gold Scores don't wrap even if their contents get long. // 0.6.5: Paints demilitarized (but neither allied nor your own) cities yellow. // 0.6.6: Added some more key bindings: d/t/p/b/s, for all(?) the city actions. // // -------------------------------------------------------------------- // // This script places an icon to the right of a players // name after selecting their town on the Island overview, // or when viewing their Town. This icon links the user to // the scoreboard, where you can see the players score. // // Feel free to have a go yourself; as long as you leave // a little credit, and of course publish for the players // of Ikariam! // // This script was originally created by ImmortalNights, // and further edited and enhanced by wphilipw and ecmanaut. // // -------------------------------------------------------------------- // // ==UserScript== // @name IkariamScoreLinker // @namespace ikariamScript // @description Adds a link to the Scoreboard besides a players name after selecting their town on the Island Overview or when viewing their Town. // @include http://*.ikariam.*/* // @exclude http://board.ikariam.*/* // ==/UserScript==

/* The startup functions and global variables. Original Author: ImmortalNights & wphilipw For version: 0.3.0 Last changed: 0.6.0

  • /

var show = { gold: 4, military: 2, total: 1 }; var post = {

   total: "score",
military: "army_score_main",
    gold: "trader_score_secondary"

};

var saving; var gameServer = location.host; var valueCache = eval(GM_getValue(gameServer, "({})")); var changeLink = GM_getValue("link", true); var whatToShow = GM_getValue("show", "7"); var inlineScore = GM_getValue("inline", true);

if ($("options_changePass"))

 displayOnOptions_fn();

else

 init();

function init() {

 function maybeLookup(e) {
   var n = $X('.//span[@class="textLabel"]', e.target);
   var ul = $X('ancestor-or-self::li[1]/ul[@class="cityinfo"]', e.target);
   if ($X('li[contains(@class," name")]', ul)) return; // already fetched!
   var who = $X('li[@class="owner"]/text()[preceding::*[1]/self::span]', ul);
   var name = trim(who.textContent);
   fetchScoresFor(name, ul, n, number(n.parentNode.id));
 }
 function lookupOnClick(a) {
   onClick(a, function(e) { setTimeout(maybeLookup, 10, e); });
 }
 function peek(e) {
   var on = e.target;
   cities.map(click);
   if (/^a$/i.test(on.nodeName))
     click(on);
 }
 if ("island" == document.body.id) {
   GM_addStyle(<><![CDATA[#island #information .messageSend img {
     position: absolute;
     margin: -3px 0 0 4px;
   }]]></>.toXMLString());
   var id = location.href.match(/[&?]id=(\d+)/);
   if (id) id = id[1];
 }
 var cities = getCityLinks();
 if (cities.length) {
   cities.forEach(lookupOnClick);
   var body = document.body;
   addEventListener("keypress", keyboard, true);
   return inlineScore && onClick(body, peek, 0, "dbl");
 }
 var player = itemValue("owner");
 if (player)
   fetchScoresFor(player, null, null, id);

}

function saveCache() {

 //console.log("Saving cache: %x", uneval(valueCache));
 GM_setValue(gameServer, uneval(valueCache).replace(/ /g, ""));

}

function cacheValue(id, type, value) {

 //console.log("caching", id, type, value);
 var city = valueCache[id] || {};
 type = type.charAt();
 city[type] = number(value);
 city.T = time();
 valueCache[id] = city;
 saving && clearTimeout(saving);
 saving = setTimeout(saveCache, 1e3);

}

function focus(direction) {

 var all = getCityLinks();
 var now = unsafeWindow.selectedCity;
 var cur = $X('id("cityLocation'+ now +'")/a') || all[all.length - 1];
 if (all.length) {
   now = all.map(function(a) { return a.id; }).indexOf(cur.id);
   click(all[(now + direction + all.length * 3) % all.length]);
 }

}

function keyboard(e) {

 function invoke(a) {
   a = $X('id("actions")/ul[@class="cityactions"]/li[@class="'+ a +'"]/a');
   return function() { if (a && a.href) location.href = a.href; };
 }
 function counterClockwise() { focus(-1); }
 function clockwise() { focus(1); }
 function tab() {
   if (!e.altKey && !e.ctrlKey && !e.metaKey)
     focus(e.shiftKey ? -1 : 1);
 }
 var keys = {
   "\t": tab, j: counterClockwise, k: clockwise,
   d: invoke("diplomacy"), t: invoke("transport"),
   p: invoke("plunder"), b: invoke("blockade"), s: invoke("espionage")
 };
 var action = keys[String.fromCharCode(e.keyCode || e.charCode)];
 if (action) {
   e.stopPropagation();
   e.preventDefault();
   action();
 }

}

function fetchScoresFor(name, ul, n, id) {

 function searchbutton(type) {
   var url = "url(/skin/" + ({
      total: "layout/medallie32x32_gold.gif) no-repeat -7px -9px",
   military: "layout/sword-icon2.gif) no-repeat 0 2px;",
       gold: "resources/icon_gold.gif) no-repeat 0 0; width:18px"
     })[type];
   return <input type="submit" name="highscoreType"
     value=" " title={"View player's "+ type +" score"}
     style={"border: 0; height: 23px; width: 16px; cursor: pointer; " +
            "color: transparent; background:"+ url}
     onclick={"this.value = '"+ post[type] +"'; this.form.submit();"}/>;
 }
 var scores = changeLink &&
   <a href="/index.php?view=options"
     title="Change score options">Change Options</a>;
 if (!inlineScore) {
   var form = <form action="/index.php" method="post">
     <input type="hidden" name="view" value="highscore"/>
     <input type="hidden" name="" id="searchfor"/>
     <input type="hidden" name="searchUser" value={name}/>
   </form>;
   for (var type in post)
     if (whatToShow & show[type])
       form.* += searchbutton(type);
   if (changeLink) {
     scores.@style = "position: relative; top: -6Px;";
     form.* += scores;
   }
   form.@style = "position: relative; "+ (changeLink ? "left:-26px; " : "") +
     "white-space: nowrap;";
   scores = form;
 }
 if (!inlineScore || changeLink)
   addItem("options", scores, ul);
 if (!inlineScore) return;
 for (type in show) {
   if (!(whatToShow & show[type]))
     continue;
   if ("gold" == type && isMyCity(ul) && viewingRightCity(ul)) {
     var gold = $("value_gold").innerHTML;
     updateItem(type, gold, cityinfoPanel(), null, lootable(gold));
     continue;
   }
   addItem(type, "fetching...");
   requestScore(name, type, id, makeShowScoreCallback(name, type, ul, n, id));
 }

}

function isMyCity(ul, name) {

 if ("city" == document.body.id)
   return $X('id("position0")/a').href != "#";
 var name = getItem("owner", ul);
 var a = $X('a', name);
 if (a) {
   var id = a.search.match(/destinationCityId=(\d+)/)[1];
   return $X('id("citySelect")/option[@value="'+ id +'"]');
 }
 var city = itemValue("name", ul);
 return $X('id("citySelect")/option[.="'+ city +'"]');

}

function lootable(score, ul) {

 var amount = parseInt((score||"").replace(/\D+/g, "") || "0", 10);
 var panel = getItem("citylevel");
 var level = getItem("citylevel", ul);
 var size = itemValue(level);
 var max = Math.round(size * (size - 1) / 10000 * amount);
 if (isNaN(max)) return;
 max = node("span", "", null, "\xA0("+ fmtNumber(max) +"\)");
 max.title = "Amount of gold lootable from this town";
 return max;

}

function viewingRightCity(ul) {

 return itemValue("name") == itemValue("name", ul) &&
       itemValue("owner") == itemValue("owner", ul);

}

function makeShowScoreCallback(name, type, ul, n, id) {

 return function showScore(xhr, cached) {
   var score = xhr;
   if ("yes" == cached) {
     score = fmtNumber(score);
   } else { // need to parse out the score
     score = $X('.//div[@class="content"]//tr[td[@class="name"]="' +
                name + '"]/td[@class="score" or @class="§"]',
                node("div", "", null, xhr.responseText));
     score = score.innerHTML;
   }
   if (score) {
     if ("yes" != cached) cacheValue(id, type, score);
     ul = ul || cityinfoPanel();
     if (n && "0" == score && "military" == type) {
       n.style.fontWeight = "bold"; // n.style.fontStyle = "italic";
       n = $X('../preceding-sibling::div[@class="cityimg"]', n);
       if (n)
         n.style.backgroundImage = getComputedStyle(n,"").
           backgroundImage.replace("red.gif", "yellow.gif");
     }
     // You rob gold (size * (size - 1)) % of the treasury of the city:
     if ("gold" == type)
       var max = lootable(score, ul);
     updateItem(type, score, ul, !!n, max);
   }
 };

}

function getCityLinks() {

 return $x('id("cities")/li[contains(@class,"city level")]/a');

}

function itemValue(item, ul) {

 var li = "string" == typeof item ? getItem(item, ul) : item;
 var xpath = 'text()[preceding-sibling::*[1]/self::span[@class="textLabel"]]';
 var text = $X(xpath, li);
 return text && trim(text.textContent || "");

}

function getItem(type, ul) {

 return $X('li[contains(concat(" ",normalize-space(@class)," ")," '+
           type +' ")]', ul || cityinfoPanel());

}

function mkItem(type, value) {

 var li = node("li", type + " name", null, value);
 var title = (type in show) ?
   type.charAt().toUpperCase() + type.slice(1) + " Score:" : "Scores:";
 li.insertBefore(node("span", "textLabel", null, title), li.firstChild);
 return li;

}

function addItem(type, value, save) {

 var li = getItem(type);
 if (li) {
   li.lastChild.nodeValue = value;
 } else {
   var ul = cityinfoPanel();
   var next = $X('li[@class="ally"]/following-sibling::*', ul);
   ul.insertBefore(li = mkItem(type, value), next);
 }
 if (save && !getItem(type, save)) {
   next = $X('li[@class="ally"]/following-sibling::*', save);
   save.insertBefore(li.cloneNode(true), next);
 }
 return li;

}

function updateItem(type, value, ul, islandView, append) {

 var li = getItem(type, ul);
 if (li) {
   li.lastChild.nodeValue = value;
 } else {
   var next = $X('li[@class="ally"]/following-sibling::*', ul);
   ul.insertBefore(li = mkItem(type, value), next);
   if (viewingRightCity(ul) && islandView) // only touch panel on right focus
     updateItem(type, value, null, null, append && append.cloneNode(true));
 }
 if (append && !$X('span[@title]', li)) {
   li.style.whiteSpace = "nowrap";
   li.appendChild(append);
 }
 return li;

}

function cityinfoPanel() {

 return $X('id("information")//ul[@class="cityinfo"]');

}

function node(type, className, styles, content) {

 var n = document.createElement(type||"div");
 if (className) n.className = className;
 if (styles)
   for (var prop in styles)
     n.style[prop] = styles[prop];
 if (content)
   n.innerHTML = "string" == typeof content ? content : content.toXMLString();
 return n;

}

function click(node) {

 var event = node.ownerDocument.createEvent("MouseEvents");
 event.initMouseEvent("click", true, true, node.ownerDocument.defaultView,
                      1, 0, 0, 0, 0, false, false, false, false, 0, node);
 node.dispatchEvent(event);

}

function fmtNumber(n) {

 n += "";
 for (var i = n.length - 3; i > 0; i -= 3)
   n = n.slice(0, i) +","+ n.slice(i);
 return n;

}

function number(n) {

 n = { string: 1, number: 1 }[typeof n] ? n+"" : n.textContent;
 return parseInt(n.replace(/\D+/g, "") || "0", 10);

}

function trim(str) {

 return str.replace(/^\s+|\s+$/g, "");

}

function onClick(node, fn, capture, e) {

 node.addEventListener((e||"") + "click", fn, !!capture);

}

function $(id) {

 return document.getElementById(id);

}

function $x( xpath, root ) {

 var doc = root ? root.evaluate ? root : root.ownerDocument : document, next;
 var got = doc.evaluate( xpath, root||doc, null, 0, null ), result = [];
 switch (got.resultType) {
   case got.STRING_TYPE:
     return got.stringValue;
   case got.NUMBER_TYPE:
     return got.numberValue;
   case got.BOOLEAN_TYPE:
     return got.booleanValue;
   default:
     while (next = got.iterateNext())
       result.push( next );
     return result;
 }

}

function time(t) {

 t = t || Date.now();
 return Math.floor(t / 6e4) - 2e7; // ~minute precision is enough

}

function $X( xpath, root ) {

 var got = $x( xpath, root );
 return got instanceof Array ? got[0] : got;

}

/* The AJAX request system so we can display the scores inline Original Author: wphilipw For version: 0.5.0 Last changed: 0.5.0

  • /

function requestScore(name, type, id, onload) {

 var cached = id && valueCache[id], key = type.charAt();
 if (cached && cached[key] && ((time() - cached.T) < 10))
   return onload(cached[key], "yes");
 //else delete valueCache[id]; // stale -- but save for now; could be useful
 GM_xmlhttpRequest({
   method: "POST",
   url: "http://" + gameServer + "/index.php",
   data: "view=highscore&highscoreType="+ post[type] +"&searchUser="+ name,
   headers: {
     "User-agent": "Mozilla/4.0 (compatible) Greasemonkey",
     "Content-type": "application/x-www-form-urlencoded",
     "Accept": "application/atom+xml,application/xml,text/xml",
     "Referer": "http://" + gameServer + "/index.php"
   },
   onload: onload
 });

}

/* runs on first run to set up default values Original Author: ImmortalNights For version: 0.5.4 Last changed: 0.6.0

  • /

function displayOnOptions_fn() {

 var mybox = node("div", "", { textAlign: "left" });
 var opts = <>

Score Display Options

Show Total Score: <input type="checkbox" id="totalScore"/>
Show Army Score: <input type="checkbox" id="militaryScore"/>
Show Gold Score: <input type="checkbox" id="goldScore"/>
Show Score Inline: <input type="checkbox" id="inlineScore"/>
Show Score Options link: <input type="checkbox" id="changeScoreOptions"/>

</>;

 mybox.innerHTML = opts.toXMLString();
 var pwd = $('options_changePass');
 pwd.appendChild(mybox);
 var checkboxes = $x('//input[@type="checkbox" and contains(@id,"Score")]');
 for (var i = 0; i < checkboxes.length; i++) {
   var input = checkboxes[i];
   var id = input.id.replace("Score", "");
   if (id == "inline")
     input.checked = !!inlineScore;
   else if ("changeOptions" == id)
     input.checked = !!changeLink;
   else
     input.checked = !!(show[id] & whatToShow);
 }
 var inputs = $x('//input[@type="submit"]');
 for (var e = 0; e < inputs.length; e++)
   onClick(inputs[e], changeShow_fn, true);

}

/* This function saves the options chosen above Original Author: wphilipw For version: 0.4.5 Last changed: 0.6.0

  • /

function changeShow_fn(e) {

 GM_setValue("show", (
               (show.total * $('totalScore').checked) |
               (show.military * $('militaryScore').checked) |
               (show.gold * $('goldScore').checked)
             ) + "");
 GM_setValue("inline", $('inlineScore').checked);
 GM_setValue("link", $('changeScoreOptions').checked);
 e.target.form.submit();

}