Planets.nu Plugin Toolkit

Library of useful functions to be used by other planets.nu userscripts

// ==UserScript==
// @name          Planets.nu Plugin Toolkit
// @description   Library of useful functions to be used by other planets.nu userscripts
// @include       http://*.planets.nu/*
// @include       http://planets.nu/*
// @version 0.5
// @namespace https://greasyfork.org/users/7189
// ==/UserScript==

/*
Version Notes
0.2     Fix local save function names in template and make names more consistent
        Small change to testChange function to exclude stock changes with notes
0.3     Add vgap.addMessageType
        Add vgap.mineFieldRadius
        Add vgap.guessBeamsFromSweep
        Add version to toolkit, so plugins can check if user needs to update
0.4     Add note storage utility funcs to pluginTemplate
0.5     Add some text processing functions:
        vgap.addLinksToText
        vgap.addLinksToMessage
*/
 
function wrapper () { // wrapper for injection

var VERSION = 5; // Use userscript version * 10


// Install better callPlugins function:
// - Doesn't require all functions to be defined
// - Calls plugin functions in plugin context, if plugin.useOwnContext == true
//   (default for toolkit plugins)
vgaPlanets.prototype.callPlugins = function (func) {
    for (var i = 0; i < vgap.plugins.length; i++) {
        var plugin = vgap.plugins[i];
        if (typeof plugin[func] === "function") {
            if (plugin.useOwnContext)
                plugin[func].call(plugin);
            else
                plugin[func].call();
        }
    }
}

// Add Message Type Function - adds a messsage type to the vgap.messageTypes array
// Returns the index assigned to the message type. Scripts should store this as it might not
// always be the same if multiple scripts are building custom messages
vgaPlanets.prototype.addMessageType = function (name) {
    return vgap.messageTypes.push(name) - 1;
}

// Utlity function to return radius of minefield given number of mines
vgaPlanets.prototype.minefieldRadius = function (mines) {
    return Math.floor(Math.sqrt(mines));
}

// Attempts to guess possible beam combinations given an amount of mines swept
vgaPlanets.prototype.guessBeamsFromSweep = function (mines, isweb) {
    var results = [];
    var mult = 4;
    if (isweb) mult = 3;
    if (mines % mult) return results;
    
    mines = Math.abs(mines) / mult;
    
    for (var id = 10; id >= 1; id--) {
        var power = id * id;
        if (mines % power == 0) {
            var count = mines / power;
            if (count > 0 && count <= 10) results.push( {beamid: id, count: count} );
        }
    }
    
    return results;
}

//
// START Storage Functions
//

// Local Storage
vgaPlanets.prototype.saveObjectLocal = function (key, obj) {
    localStorage.setItem(key, JSON.stringify(obj));
}

vgaPlanets.prototype.loadObjectLocal = function (key) {
    var val = localStorage.getItem(key);
    if (val == null) return null;
    return JSON.parse(val);
}

// Game Notes
vgaPlanets.prototype.saveObjectNote = function (id, type, obj) {
    var note = this.getNote(id, type);
    if (note == null)
        note = this.addNote(id, type);
    note.changed = 1;
    note.body = JSON.stringify(obj);
}
    
vgaPlanets.prototype.loadObjectNote = function (id, type) {
    var note = this.getNote(id, type);
    if (note != null && note.body != "")
        return JSON.parse(note.body);
    else
        return null;
}

//Aliases to old function names for legacy scripts
vgaPlanets.prototype.saveObjectAsNote  = vgaPlanets.prototype.saveObjectNote;
vgaPlanets.prototype.getObjectFromNote = vgaPlanets.prototype.loadObjectNote;

// Local File

vgaPlanets.prototype.getObjectExportLink = function (obj, filename) {
    var link = $( "<a/>", {
        href: "file://" + filename,
        text: filename,
        download: filename,
        title: filename,
        click: function(ev) { $(this).attr("href", "data:," + JSON.stringify(obj)); return true; }
    });
    return link;
}

//
// END Storage Functions
//

//
// START Text Processing Functions
//

// Parses the string "value", replacing common message shorthands with full length linked versions
// ie. "s#42" or "s42" will be replaced with "S#42: ONYX CLASS FRIGATE" and when that text is clicked on
// it will open that object on the map
// index parameter may be omitted. It is included in the definition so it can be passed to jQuery.html()
vgaPlanets.prototype.addLinksToText = function (index, value) {
    if (arguments.length == 1)
        value = index;
    return value.replace(/\b([spb])#?([0-9]{1,3})\b/gi, function (match, type, textid) {
        id = parseInt(textid, 10);
        var jumpfunc = "";
        var mouseover = "";
        var target = null;
        switch (type.toUpperCase()) {
            case "P":
                target = vgap.getPlanet(id);
                if (target != null) {
                    jumpfunc = ( vgap.player.id == target.ownerid ? "vgap.map.selectPlanet(" + id + ");" : "shtml.planetSurvey(" + id + ");" );
                }
                break;
            case "S":
                target = vgap.getShip(id);
                if (target != null) {
                    jumpfunc = ( vgap.player.id == target.ownerid ? "vgap.map.selectShip(" + id + ");" : "shtml.shipSurvey(" + id + ");" );
                    mouseover = " onmouseover='vgap.showHover(" + id + ");' onmouseout='vgap.hideHover();'";
                }
                break;
            case "B":
                target = vgap.getStarbase(id);
                if (target != null) {
                    target = vgap.getPlanet(target.planetid);
                    jumpfunc = ( vgap.player.id == target.ownerid ? "vgap.map.selectStarbase(" + id + ");" : "shtml.planetSurvey(" + id + ");" );
                }
                break;                         
        }
        if (target != null) {
            var link = "<a style='color:cyan;' onclick='vgap.showMap();vgap.map.centerMap(" + target.x + ", " + target.y + ", true);" + jumpfunc + "return false;'" + mouseover + " >";
            link += match.toUpperCase() + ": " + target.name + "</a>";
            return link;
        }
        else
            return match;
    });
}

// Like above function, but creates links for system message formats instead
// ie. ID#123
vgaPlanets.prototype.addLinksToMessage = function (index, value) {
    if (arguments.length == 1)
        value = index;        
    var matches = value.match(/ID#[0-9]{1,3}/g);
    //console.log(matches);
    if (matches != null) {
        for (i=0; i<matches.length; i++) {
            var id = parseInt(matches[i].substring(3));
            var ship = vgap.getShip(id);
            if (ship != null) {
                value = value.replace( new RegExp(ship.name.replace(/[-[\]{}()*+?.,\\^$\|#\s]/g, "\\$&") + " " + matches[i], "g"), function (match) {
                    jumpfunc = ( vgap.player.id == ship.ownerid ? "vgap.map.selectShip(" + id + ");" : "shtml.shipSurvey(" + id + ");" );
                    mouseover = " onmouseover='vgap.showHover(" + id + ");' onmouseout='vgap.hideHover();'";
                    return "<a style='color:cyan;' onclick='vgap.showMap();vgap.map.centerMap(" + ship.x + ", " + ship.y + ", true);" + jumpfunc + "return false;'" + mouseover + " >" + match + "</a>"; 
                });
            }
            var planet = vgap.getPlanet(id);
            if (planet != null) {
                value = value.replace( new RegExp(planet.name.replace(/[-[\]{}()*+?.,\\^$\|#\s]/g, "\\$&") + " " + matches[i], "g"), function (match) {
                    jumpfunc = ( vgap.player.id == planet.ownerid ? "vgap.map.selectPlanet(" + id + ");" : "shtml.planetSurvey(" + id + ");" );
                    mouseover = "";
                    return "<a style='color:cyan;' onclick='vgap.showMap();vgap.map.centerMap(" + planet.x + ", " + planet.y + ", true);" + jumpfunc + "return false;'" + mouseover + " >" + match + "</a>"; 
                });
            }
        }                
    }
    return value;
}

//
// END Text Processing Functions
//
 
//
// START Bundle Save
//
 
vgapSaveBundle = function (id) {
    this.id = id;
    this.data = new dataObject();
    this.objs = [];
}
vgapSaveBundle.prototype = {
    add: function(obj) {
        this.objs.push(obj);
    },
    datalength: function() {
        return this.data.data.length;
    },
    merge: function(bundle) {
        this.data.data += bundle.data.data;
        this.objs = this.objs.concat(bundle.objs);
    },
    save: function(callback) {
    
        if (!callback)
            callback = function (result) {vgap.processBundle(result)};
        var out = new dataObject();
        out.add("gameid", vgap.gameId);
        out.add("playerid", vgap.player.id);
        out.add("turn", vgap.settings.turn);
        out.add("version", vgap.version);
        out.add("savekey", vgap.savekey);
        out.add("apikey", vgap.apikey);
        out.add("saveindex", 2);
        var keys = 7;
        keys++; //keycount=

        if (typeof WinJS == 'undefined')
            keys += 2; //2 for jsoncallback=? and jquery unique id
        else
            keys += 1; //random cache preventer

        if (nu.isfacebook)
            keys -= 2;
        out.data += this.data.data;
            keys += this.objs.length;
        out.add("keycount", keys);

        vgap.request("/game/save", out, callback);
    }
}

vgaPlanets.prototype.processBundle = function (result) {
    var bundle = vgap.savebundles.shift();
    if (result) {
        if (result.success) {
            console.log("bundle OK");
            //mark completed things as saved.
            for (var i = 0; i < bundle.objs.length; i++) {
                var obj = bundle.objs[i];
                if (obj === "Relations") {
                    if (this.relationChanged == 2) this.relationChanged = 0;
                }
                else {
                    if (obj.changed == 2) obj.changed = 0;
                }
            }
        }
        else {
            if (result.error == "INVALID SAVE KEY")
                vgap.disabled();
            else if (result.error.toLowerCase().indexOf("data validation error") >= 0) {
                alert("A data validation error has occured. This is most likely caused by a disconnection or failed save. You will need to reload your turn to prevent any data from being corrupted.");
                this.readOnly = true;
                this.indicator.text("Read Only");
                this.indicateOn();
            }
            else
                alert(result.error);
        }
        if (this.disconnect) {
            this.disconnect = false;
            this.indicateOff();
        }
    }
    else {
        //We appear to be disconnected from the internet.  Warn the user.
        vgap.disconnected();
    }
    
    this.sendBundles()
}            

vgaPlanets.prototype.getSaveBundle = function(id) {
    var bundle = null;
    // If no id, just make a new blank bundle
    if (typeof id === "undefined" || id == null)
        bundle = new vgapSaveBundle(null);
    
    // otherwise, look through the current list and return match if found
    else for (var i = 0; i < this.savebundles.length; i++ && bundle == null) {
        var b = this.savebundles[i];
        if (b.id === id)
            return b;
    }
    
    // not found, id specified, make new bundle with that id
    bundle = new vgapSaveBundle(id);
    
    // if we made it this far, should be a new bundle. Add it to the list and return it.
    this.savebundles.push(bundle);
    return(bundle);
}


// Goes through all saveable objects, and bundles them up according to data validation
// requirements. ie. Everything in the same location is saved together.
// Result is a populated vgap.savebundles array with items that can be saved directly
// via vgap.bundleSave(), or packed farther with vgap.bundlePack()
vgaPlanets.prototype.buildBundles = function () {       
            
    vgap.savebundles = [];

    //order of these sets is important because of data validation code.
    for (var i = 0; i < this.myships.length; i++) {
        if (this.myships[i].changed > 0) {
            var bundle;
            var ship = this.myships[i];
            //data validation: if we save a ship, we always save our own planet if at same location
            var planet = vgap.planetAt(ship.x, ship.y);
            if (planet != null && planet.ownerid == ship.ownerid) {
                planet.changed = 1;                
                bundle = vgap.getSaveBundle(planet.id);
            }
            else {
                bundle = vgap.getSaveBundle(ship.x + "-" + ship.y);
            }
            bundle.add(ship);
            bundle.data.add("Ship" + ship.id, this.serializeShip(ship), false);
            ship.changed = 2;
        }
    }
    for (var i = 0; i < this.stock.length; i++) {
        var item = this.stock[i];
        if (item.changed > 0 && !item.inserted) {
            //data validation: if we save a stock, we always save the planet
            var starbase = vgap.getStarbaseById(item.starbaseid);
            var planet = vgap.getPlanet(starbase.planetid);
            planet.changed = 1;
            var bundle = vgap.getSaveBundle(planet.id);
            bundle.add(item);
            bundle.data.add("Stock" + item.id, this.serializeStock(item), false);
            item.changed = 2;
        }
    }
    for (var i = 0; i < this.myplanets.length; i++) {
        if (this.myplanets[i].changed > 0) {
            var planet = this.myplanets[i];
            var bundle = vgap.getSaveBundle(planet.id);
            bundle.add(planet);                
            bundle.data.add("Planet" + planet.id, this.serializePlanet(planet), false);
            planet.changed = 2;

            //data validation: if we save a planet, we always save the starbase as well
            var starbase = vgap.getStarbase(planet.id);
            if (starbase != null)
                starbase.changed = 1;
        }
    }
    for (var i = 0; i < this.mystarbases.length; i++) {
        if (this.mystarbases[i].changed > 0) {
            var starbase = this.mystarbases[i];
            var bundle = vgap.getSaveBundle(starbase.planetid);
            bundle.add(starbase);                  
            bundle.data.add("Starbase" + starbase.id, this.serializeStarbase(starbase), false);
            starbase.changed = 2;
        }
    }
    for (var i = 0; i < this.notes.length; i++) {
        if (this.notes[i].changed > 0) {
            var bundle = vgap.getSaveBundle();
            bundle.add(this.notes[i]);             
            bundle.data.add("Note" + this.notes[i].targetid + this.notes[i].targettype, this.serializeNote(this.notes[i]), false);
            this.notes[i].changed = 2;
        }
    }
    if (this.relationChanged > 0) {
        var bundle = vgap.getSaveBundle("Relations");        
        for (var i = 0; i < this.relations.length; i++) {
            bundle.add("Relations");
            bundle.data.add("Relation" + this.relations[i].id, this.serializeRelation(this.relations[i]), false);
        }
        this.relationChanged = 2;
    }
    console.log("BUILD: " + this.savebundles.length);
}

vgaPlanets.prototype.packBundles = function(sizelimit) {
    if (!sizelimit) sizelimit = this.bundlesize;
    if (!sizelimit) sizelimit = 15500;
    var packed = [];
    while (this.savebundles.length > 0) {
        var bundle = this.savebundles.shift();
        bundle.id = "Packed";
        packed.push(bundle);
        for (var i = 0; i < this.savebundles.length; i++) {
            testbundle = this.savebundles[i];
            if (bundle.datalength() + testbundle.datalength() <= sizelimit) {
                bundle.merge(testbundle);
                this.savebundles.splice(i, 1);
                i--;
            }
        }         
    }
    
    this.savebundles = packed;
    console.log("PACK:  " + this.savebundles.length);    
}

// Only actually sends the first bundle, but callbacks should keep calling this until
// vgap.savebundles is empty (everything has been attempted)
vgaPlanets.prototype.sendBundles = function() {
    if (this.savebundles.length == 0) {
        this.saveInProgress = 0;
        return;
    }
    this.savebundles[0].save();
}

vgaPlanets.prototype.bundleSave = function () {

    if (this.inHistory)
        return;
    
    if (this.game.status == 3) //finished
        return;
    
    if (this.readOnly) {
        alert("Data will not be saved. This window is read only.");
        return;
    }
    
    //only one save at a time, or we can get them arriving out of order
    if (this.saveInProgress > 0)
        return;
    
    this.saveInProgress = 2;
        
    this.buildBundles();
    this.packBundles();
    this.sendBundles();        

}

//
// END Bundle Save
//


// These Have Changed - commenting out for now
/*
vgapMap.prototype.zoomlevels = [
    0.2,
    0.4,
    0.6,
    0.8,
    1,
    1.4,
    2,
    2.8,
    3.9,
    5.5,
    7.7,
    10.8,
    15.1,
    21.1,
    29.5,
    41.3,
    57.8,
    80.9,
    113.3,
    158.6,
    222.2,
    310.8,
    435.1,
    609.1
];
*/

pluginTemplate = function() {};
pluginTemplate.prototype = {

    isToolkitPlugin: true,
    useOwnContext: true,
    
    saveObjectLocal: function (key, obj, isGameSpecific) {
        var savekey = this.name;
        if (isGameSpecific) savekey += "." + vgap.game.id;
        savekey += "." + key;
        vgap.saveObjectLocal(savekey, obj);
    },
    
    loadObjectLocal: function (key, isGameSpecific) {
        var savekey = this.name;
        if (isGameSpecific) savekey += "." + vgap.game.id;
        savekey += "." + key;
        return vgap.loadObjectLocal(savekey);
    },
    
    saveObjectNote: function (id, object) {
        vgap.saveObjectNote(id, this.notetype, object);
    },
    
    loadObjectNote: function (id) {
        return vgap.loadObjectNote(id, this.notetype);
    }   

}

vgapPluginToolkit = function() {};
vgapPluginToolkit.prototype = {
    version: VERSION,
    makeToolkitPlugin: function (plugin) {
        if (!plugin.isToolkitPlugin)
            plugin.__proto__ = pluginTemplate.prototype;
        if (!plugin.notetype)
            plugin.notetype = -(this.bigintHash(plugin.name));
    },
    registerPlugin: function (plugin) {
        this.makeToolkitPlugin(plugin);
        vgap.registerPlugin(plugin, plugin.name);
    },
    bigintHash: function (string) {
        return parseInt(string.replace(/[_\W]/g, ""), 36) % 2147483648;
    }
    
}   

vgaPlanets.prototype.toolkit = new vgapPluginToolkit();



// Test function to mark everything as changed
// Mainly used for testing bundleSave
vgaPlanets.prototype.testChanged = function (changeState, excludenotes) {

    //set every object which has a change state of saveIndex to changeState 
    for (var i = 0; i < this.myplanets.length; i++) {
            this.myplanets[i].changed = changeState;
    }
    for (var i = 0; i < this.myships.length; i++) {
            this.myships[i].changed = changeState;
    }
    for (var i = 0; i < this.mystarbases.length; i++) {
            this.mystarbases[i].changed = changeState;
    }
    

    this.relationChanged = changeState;

    if (excludenotes) return;  
    for (var i = 0; i < this.stock.length; i++) {
        var item = this.stock[i];
            item.changed = changeState;
    }    
    for (var i = 0; i < this.notes.length; i++) {
            this.notes[i].changed = changeState;
    }
    
}


} //wrapper for injection

var script = document.createElement("script");
script.type = "application/javascript";
script.textContent = "(" + wrapper + ")();";

document.body.appendChild(script); 
document.body.removeChild(script);