Scripting tutorial for starters. Created by @ciastex and @i20k.
Special thanks to @dtr/@sudo, @ada and @soron for their expertise.
Ported to forums by @sybil. DM @sybil#7258 on discord for correction or edit suggestions.
Related reading:
`P
https://hackmud.com/forums/general_discussion/common_script_errors_and_causes`
If you have any questions, don't hesitate to hit us on Discord:
`Phttps://discord.gg/sc6gVse`
`LGETTING SLOTS`
Script slots allow you to upload a script. Every script uses one script slot
Public script slots allow you to make an uploaded script public. This uses a public script slot additionally to the regular script slot the script takes
In order to use a script slot, you must load a script slot upgrade
To load upgrades, see `Fsys`.`Lupgrades` and then use `Fsys`.`Lmanage` to load the upgrade.
The names of the upgrades that you load to add slots are public_slot_v1 (or v2 or v3) and script_slot_v1 (or v2 or v3)
These upgrades can be obtained from tier 1 NPCs (or t2 NPCs for the _v2 variants) or from `Fmarket`.`Lbrowse`.
In order to see how many slots you have equipped, use `Fsys`.`Lspecs`.
Also, if you want to see which scripts you currently have on the server, run `Fscripts`.`Luser`.
NOTE: all items in the market (that is, `Fmarket`.`Lbrowse`) are trustworthy. The descriptions are user provided and cannot be trusted, but the items themselves are entirely game-controlled and cannot be tampered with.
`LARCHITECT COMMANDS`
`C#help`
This will show you the list of available commands
`C#edit <filename>`
This command will create or edit a script, opening it up with your default text editor. If you are on Windows this may crash, as the default .js command on Windows is the system built-in compiler. To fix this issue, associate your text editor with the .js file extension. If you don’t have a text editor, get one like Notepad++. Scripts will be created with a default template.
`C#dir`
This command simply opens up your script directory. You can create new scripts here and them upload them ingame
See [3] if neither command works for you on linux/windows
`C#up <filename>`
This command will upload your created script to the server, so you can execute it.
Possible arguments AFTER the filename:
- delete: will delete your script from the server, but leave it locally.
- public: will make your SCRIPT public - assuming you have the public slot upgrade installed and loaded within your system.
- private: will explicitly mark a script as private (useful to un-public a script while debugging, for example)
- shift: will 'shift' your script. This is necessary if the security level changes, or you make it public for the first time (or the first time after deleting it). Takes 15 minutes, during which no one can interact with the script
`C#down <filename>`
This command will download a copy of the script from the server. If the file already exists, will be downloaded as filename.down.js
`C#DELETE <filename>`
This command will remove your script from your computer’s file system, which means you won’t be able to access it from your editor anymore. Be careful around this command, though - it runs `Dwithout any confirmation`. Note: if the script was previously uploaded, the server copy will still exist, and can be downloaded again with #down
`C#scripts` [alt: `C#`]
This command will list all your local and uploaded scripts. To see your currently uploaded scripts, run `Fscripts`.`Luser`
`LSCOPE OF SCRIPTS`
All scripts have ability to be public or private - this allows to limit your script exposure to others. However, you `Mcan` call your own private script from within your own public script (or as a scriptor passed to anyone's script) You can use that to, for example, control code library availability to 3rd parties.
`LSCRIPTING`
Scripts in hackmud are JavaScript (es6 strict mode) files consisting of a single function which is passed two parameters by the game:
- context: This is a context the script is run from, i.e. if a user called noob ran your script, then any command executed from context will be treated as executed by the noob user, just like he/she would write them in their command line. Context has the following subkeys:
-- caller: The name of the user who is calling the script (i.e. n00b)
-- this_script: The name of this script
-- calling_script: The name of the script that directly called this script, or null if called on the command line or as a scriptor
-- is_scriptor: true if the script is being run as a scriptor, otherwise falsey (not present currently, but I wouldn’t rely on that)
-- is_brain: true if the script is being run via a bot brain
-- cols: the number of columns in the caller’s terminal, if reported by the client
-- rows: the number of rows in the caller’s terminal, if reported by the client
- args: This is a dictionary containing all the arguments a user passed to your script. If the script was called without any arguments (i.e. foo.bar), args will be null (if called on the command line) or undefined (if called as a scriptor or subscript). If called with empty arguments (i.e. `Cfoo`.`Lbar`{}), args will be an empty JS object.
Example ez_21 crackers:
`Wfunction`(context,args) { `l// target:#s.some.npc`
`Wvar` g={},r,l
`Wfunction` c() {
r=args.target.call(g);
l=r.split(`P'\n'`).pop();
}
`Wvar` EZ=[`P'open'`,`P'release'`,`P'unlock'`]
c()
`Wfor`(;;) {
if(l==`P"Connection terminated."`)`Wreturn` g
`Wif`(l.includes(`P'EZ_21'`)) {
`Wfor`(`Wvar` i=`P0`;i<EZ.length;++i) {
g.ez_21=EZ[i]
c()
`Wif`(!l.includes(`P'is not the correct'`))`Wbreak`
}
}
}
}
`cEd note: The following code is severely outdated and also not that useful. Retaining it for historical value; use the above sample code instead`
`Wfunction`(context, args)
{
`l///usage ez_21{target:#s.your.target}`
`Wvar` c=[`P"open"`, `P"release"`, `P"unlock"`];
`Wvar` llen = `P"'NLOCK_UNLOCKED"`.length;'`c(ED NOTE: forum colorcodes required that i changed the backtick to an apostrophe here)`
`Wvar` ret = "";
`Wvar` success = false;
`Wfor`(`Wvar` k=`P0`; k<`P3`; k++)
{
`l///alt syntax`
`l/*var v = {};`
`lv["ez_21"] = c[k];`
`lret = args.target.call(v);*/`
ret = args.target.call({ez_21 : c[k]})
`Wif`(ret.substr(`P0`, llen) === `P"'NLOCK_UNLOCKED"`) `c(ED NOTE: here too)`
{
success = true;
`Wbreak`;
}
}
`l///example to how to make a basic account transfer, makes the script medsec`
`l///this \ is to prevent this from being thought of as medsec by the game when commented out`
`l//#s.accts.xfer\_gc_to({ to:"username", amount:"5KGC" });`
`Wreturn` {ok:success, msg:ret};
}
`LCHARACTER COUNTS`
Hackmud does not count whitespace characters (space, tab, carriage return, newline, vertical tab, and possibly a few others), and does not count `A//` comments. All other forms of comments are counted. Hackmud ignores `A//` comments by replacing `A//` through the end of a line with the empty string. The parser is not smart enough to know that `A//` inside a string isn’t a comment. Thus,
`Wvar` `Ax=``P"http://google.com"`
will get mangled -- hackmud will truncate the line to
`Wvar` `Ax=``P"http:`
which will cause a syntax error. The easiest fix is to use `A/\/` anywhere you want `A//` to appear.
`LSCRIPTORS`
Scriptors are one of the hackmud specific features. They allow you to call an in-game script from your script. That allows you to parametrize your script’s behavior. The scriptor syntax is as follows:
`V#s.``Ca_user``V.``La_command`
The above can be then passed to your script as an argument, like the following (assuming you `C#up`-ped the script above as crk_ez21):
crk_ez21 { `Ntarget`:`V#s.``Ca_user``V.``La_command` }
To call a command the scriptor points to, there’s a scriptor-specific method which optionally accepts your arguments that will be passed to the called command:
`Aargs.target.call({` `l/* optional arguments for the called scriptor */` `A})`
`LSUBSCRIPTS`
If you want to call a hard-coded script `c(ed note: this isn’t a scriptor, it is just a script call, or a subscript)`, you can do so without using a scriptor, as follows. Be aware, you cannot store a script to a variable like this:
`Wvar` `Ax = #fs.user.name()`
as `A#fs` is really a preprocessing directive. `A#fs.user.name` must be used immediately, in the form
`A#fs.user.name({key:value})`
If you want to hard-code a script call that you can reuse, define a wrapper function, like:
`Wfunction` foo(args) {
`Wreturn` #fs.user.name(args);
}
As of 20/10/2017, the previous syntax #s.user.name will result in a nullsec script for subscripts (ED NOTE: now it's a syntax error). The syntax for scriptors is still #s.user.name and works properly. The new syntax is as follows:
#fs.a_user.a_command or #4s.a_user.a_command
Sec levels go from 0 (nullsec) to 4 (fullsec), or ns, ls, ms, hs, and fs. These guarantee that the calling script is *at least* of that sec level, otherwise returns {`Nok`:`Vfalse`, `Nmsg`:`V""`}
`LCONVERTING A STRING (like "``Cfoo``L.``Lbar") INTO A CALLABLE`
Many people want to take a string, like a loc from an NPC corp, and call it directly inside another script. This is, deliberately, impossible in hackmud. If you could convert a string into a callable in any way, the entire security level system would fall apart (because any string in any dependency could possibly be a nullsec script. And those strings could come from the database). If you want to do something with those locs (or similar cases), you will have to pass them in as a scriptor or hard-code them in the file. You cannot call the string directly.
As of the new seclevel update, the above security concerns are no longer an issue as you can specify the security level you’re expecting a script to be. You still cant convert a string into a callable, but we may get this in the future
`LRETURNING A RESULT`
A called script can return basically anything - an array, a string, an object, or even null. Most player scripts in the game however simply return a string, whereas most trust scripts return {ok:true_or_false, msg:arbitrary_result}
Your script itself generally returns both {`Nok`:`Vtrue`, `Nmsg`:`V"string"`}
The contents of string will automatically be printed to your terminal
Both of these arguments are optional, and while you may get an error message if you return nothing from a script, it will still work fine
`LAUTOCOMPLETE`
To add autocomplete args to your script, on the first line, after the function boilerplate, add a comment with a list of args and values, like this:
`Wfunction`
(context,args) { `l// arg1:val1, arg2:val2, arg3:#s.an.example, any_name:”example”`
...
}
After `C#up`-ing the script, you might need to run scripts.user to update your autocomplete, and then it should work.
`LSPECIAL SCRIPT COMMANDS`
`A#D(ob)` -- Debug Log
If `A#D` is called in a script you own, the return value of the top level script is suppressed and instead an array of every `A#D`’d entry is printed. This lets you use `A#D` kind of like console.log. `A#D` in scripts not owned by you are not shown. `A#D` returns its argument unchanged, so you can do things like return `A#D(ob)` to return the object when the caller isn’t you, and debug-log it when it is you (allowing you to “keep” your returns with other debug logs). `A#D`’d items are returned even if the script times out or errors.
`A#FMCL` -- Function Multi-Call Lock
`A#FMCL` is what escrow.charge uses to ensure it is only called once per script execution. The first time (in each script) that `A#FMCL` is encountered, it returns falsey, and every time thereafter it returns truthy. A common usage pattern is
`Wif`(`A#FMCL`)
`Wreturn` `P"error"`
`l// do work`
The first time that block of code is hit, it will do work, and every time after it will return the error (this applies even if (and specifically for the case where) your script is called multiple times in the same execution)
#G -- Global
#G is a per-script global object. It starts out blank, and you can add whatever properties you want to it. If your script is called multiple times in a single overall script run, its #G is persisted between those calls (but each script sees its own #G). This is useful to cache db lookups that won’t change in scripts that expect to be called many times. Sample usage:
`Wifa`(!`A#G`.my_db_entry)
`A#G`.my_db_entry=#db.f({whatever:true}).first();
`l// use #G.my_db_entry in code below`
`LSPECIAL SCRIPT VARIABLES`
`P_START` (`P_ST`)
This contains a JS timestamp (not Date) set immediately before your code begins running. You can see how much time remains by doing Date.now()-`P_START`
`P_END`
This contains a JS timestamp for the end of your script run -- effectively just
`P_ST`+`P_TO`
`P_TIMEOUT` (`P_TO`)
This contains the number of milliseconds a script is allowed to run for. Effectively always just 5000, except when a trust script is called on the command line and its value is, presumably, 8000.
`Fscripts``L.lib -- A USEFUL SUBSCRIPT`
This is a code library containing useful helper functions you can use in your scripts. Most of its functions are covered by [1] in the Misc section. You can iterate on this object to discover all of them.
`LMACROS`
Macros are fairly simple, and very useful in hackmud. This is not strictly coding related, but they are not that widely known. Example:
/macroname = test{target:"canhavefixedarguments"}
/hl = kernel.hardline
/dc = kernel.hardline{`Ndc`:`Vtrue`}
Running /macroname or /hl or /dc will run exactly that command.
Macros can have arguments. Simply double any legitimate { and } in the macro, and use {0}, {1}, etc, as arguments. Arguments can be quoted, but the quotes are included in the expansion. For example:
/c = chats.send {{ `Nchannel`:`V"{0}"`,`Nmsg`:`V{1}` }}
/c 0000 "This is a test"
`L#db`
Each users’ database in hackmud is a MongoDB collection, in which data is stored as JSON documents.
`LQUERY OBJECTS`
Query Objects are a regular JS object containing keys and values you want to search against.
See https://docs.mongodb.com/manual/reference/operator/query/ for more details.
`LPROJECTIONS`
Projections allow you to fetch specific subfields in a #db object. These speed things up quite a bit if your document is large.
Check https://docs.mongodb.com/v3.0/tutorial/project-fields-from-query-results/ for more information.
`L#db.i(document)`
Insert:
https://docs.mongodb.com/manual/reference/method/db.collection.insert/
This command creates new #db documents.
Called like #db.i(<object>);
Ex: #db.i({ SID:”scriptname” }) Inserts a document with key “SID” and value “scriptname”
If the object is an array of non-array objects, each element of the array is inserted as its own document.
`L#db.r(query)`
Remove:
https://docs.mongodb.com/manual/reference/method/db.collection.remove/
This command deletes #db documents matching your query.
Called like #db.r({query});
Ex: #db.r({ SID:”scriptname” }) removes all documents where key “SID” contains the value “scriptname”.
`L#db.f(query,projection)`
Find:
https://docs.mongodb.com/manual/reference/method/db.collection.find/
This command returns any documents matching your query.
Called like #db.f({query}, {projection}).command() where “command” is either “first” or “array” (less common commands: distinct, sort, limit, skip, and others)
Ex: #db.f({ SID:”scriptname” }).array() returns an array of documents where key “SID” contains the value “scriptname”.
Ex: #db.f({ SID:”scriptname” }, { field:1, _id:0 }).first() returns the value for the key “field” inside the first document it finds where key “SID” contains the value “scriptname”.
`L#db.u(query,command)`
Update:
https://docs.mongodb.com/manual/reference/method/db.collection.update/
This command updates any pre-existing documents matching the query.
Called like #db.u({query}, { updateOper:{updatedfields} }) applies “update” to any documents matching the query.
Ex: #db.u({ SID:”scriptname” }, { $set:{field:”new value”} }) sets key field to “new value” in any documents where key “SID” contains the value “scriptname”.
This can be a very complex operation. It is HIGHLY recommended you follow the aforementioned hyperlink.
`L#db.u1(query,command)`
Update 1
This command is nearly identical to #db.u(), except that it will update at most one document (the one that you would get if you used cursor.first() with the same query). One other change is that you can simply pass your object as the 2nd argument, with no $set or other mongo commands, to replace the object entirely (note that this can be prone to race conditions).
`L#db.us(query,command)`
Upsert
This command is nearly identical to #db.u(), if no documents match the query, one document will be inserted (“upserted”) based on the properties in both the query and the command. The $setOnInsert operator is useful to set defaults.
`L#fs.``Fscripts``L.lib`
`Aok()`
This helper method is equivalent to return {ok:true}. Note: you have to return the result yourself.
`Anot_impl()`
This helper method is equivalent to return {ok:false, msg:"not implemented"}. Note: you have to return the result yourself.
`Alog(message)`
Pushes a string representation of a value onto an array of log messages. This compensates (at the time of writing) the disability to print messages to stdout on-the-fly. It does not write anything to stdout itself. You have to use the method below.
`Aget_log()`
Returns the array used by the log() function, which you can then access. Does not clone or clear the array afterwards; it's a direct reference to the same array, which means you have to clear it after you’re done with one thing and want to use it with a second thing.
`Arand_int(min, max)`
Returns a random integer between min and max.
`Aare_ids_eq(id1, id2)`
Tests whether id1 and id2 values are equal. Apparently buggy at the moment.
`Ais_obj(what)`
Returns true if what is an Object (note that arrays are Objects).
`Ais_str(what)`
Returns true if what is a String.
`Ais_num(what)`
Returns true if what is a Number. This treats NaN (not a number) as not a number, even though in JS, typeof NaN == “number”.
`Ais_int(what)`
Returns true if what is is both a Number (via is_num), and also an integer.
`Ais_neg(what)`
Returns true if what is is both a Number (via is_num), and also negative (i.e. <0).
`Ais_arr(what)`
Returns true if what is an Array.
`Ais_func(what)`
Returns true if what is a Function.
`Ais_def(what)`
Returns true if what is defined (that is, not undefined -- ed note: null and undefined are VERY different things. This handles only undefined. A null variable is still defined -- it is defined as null).
`Ais_valid_name(what)`
Returns true if what is a valid user/script name (i.e. containing only a-z, _, and 0-9, and not starting with a number. There might also be a length limit).
`Adump(obj)`
Returns a string representation of the obj argument.
`Aclone(obj)`
Returns a clone of the obj argument (meaning references are broken).
`Amerge(obj1, obj2)`
Merges the contents of obj2 into obj1. This can be useful for combining defaults with user-specified values, but it is not quite secure on its own (i.e. don’t trust it to secure DB filters).
`Aget_values(obj)`
`c(ED NOTE: this was empty, I'll fill this in later)`
`Ahash_code(string)`
Returns a number calculated based on the string argument.
`Ato_gc_str(num)`
Converts raw num number to a GC currency representation.
`Ato_gc_num(str)`
Converts GC currency representation to a raw number.
`Ato_game_timestr(date)`
Converts a Date object specified via date parameter to a game-styled time string.
`Acap_str_len(string, length)`
Truncates the given string to the given length if it's longer than that.
`Aeach(array, fn)`
Runs fn on each array element. The fn function signature is specified in [4] at Misc, and stays the same for all filtering functions.
`Aselect(array, fn)`
Returns a collection of values from array that matches the fn predicate. If the predicate returns true, the select function adds the key:value pair currently processed to the returned collection of values.
`Acount(array, fn)`
Returns a number of items from array that matches the fn predicate. If the predicate returns true, the count function increments the returned number by one.
`Aselect_one(array, fn)`
Same as the select function, but returns the first value that matches the predicate.
`Amap(array, fn)`
Applies the fn function to each array element. The function-returned value is then stored in the map-returned array at the same index as currently processed value’s index.
`Ashuffle(array)`
Shuffles an array and returns it.
`Asort_asc(..?)`
Likely sorts an array using the num_sort_asc sorting function, unknown at the moment
`Asort_desc(..?)`
`c(ED NOTE: empty)`
`Anum_sort_asc(one, two)`
If one > two, returns 1. If two is greater than one, return -1. Else return 0. Looks like a sorting function
`Anum_sort_desc(one, two)`
Returns the opposite of the above, ie -1 on one > two, and 1 on two > one
`Amax_val_index(array)`
Returns the index of the item in the array that has the maximum value
`Aadd_time(date, add_ms)`
Gets the date of date + add_ms (milliseconds)
`Asecurity_level_names[security_level]`
An array containing names of the security levels (NULLSEC, LOWSEC, MIDSEC, HIGHSEC, FULLSEC)
`Aget_security_level_name(security_level)`
Takes a parameter between 0 and 4 (inclusive), returns the corresponding security from NULLSEC (0) to FULLSEC (4)
`Acreate_rand_string(len)`
Returns a random string consisting of lowercase alphanumeric characters.
`Aget_user_from_script(script_name)`
Returns the user from a script name. Ie me.target returns me
`Au_sort_num_arr_desc(array)`
Returns the array argument sorted in the descending fashion. This duplicates the array, e.g. you need to use the returned value in order to use the sorted array.
`Acan_continue_execution(time_left)`
Checks if the script can continue execution in the given time frame. The maximum time frame of a running script is 5000 milliseconds. If the given value is in the range of available execution time, the script returns true otherwise it returns false.
`Acan_continue_execution_error()`
If the script cannot continue execution in the given time frame (see above), this method returns the reason why the above routine returned false.
`Adate()`
`c(ED NOTE: empty)`
`Aget_date()`
Gets the current date
`Aget_date_utcsecs()`
Gets the current time from the date (ie Date.getTime())
`LJS GLOBAL OBJECTS`
A number of standard JS global objects behave differently in hackmud than in normal JS. Objects like Object, Array, JSON, Date, RegExp, and the like, have been made const and nonmodifiable to prevent wrapping scripts from tampering with them (You may see references to OBJECT, ARRAY, JSON_, DATE, and REGEXP objects in old [ancient] code, but these have been removed. The normal JS versions should be used instead). Additionally, the two primary methods on JSON, JSON.parse and JSON.stringify, have been modified so that they will never throw an error. The 2nd argument should be an empty object, which will be given an error message in the event of error. The standard JSON additional arguments are each shifted to the right by 1. This means they have the following signature in Hackmud:
JSON.stringify(object[, error[, replacer[, space]]])
JSON.parse(string[, error[, reviver]])
(JSON.ostringify and JSON.oparse are the original variants)
`LRECREATING OBJECT ORIENTED JS W/O this`
Scripts in hackmud cannot use the keyword this for safety reasons, which makes standard JS object oriented code mostly useless. However, most of OO stuff can be recreated by using factory methods. Consider the following non-hackmud-JS code:
`Wfunction` Rectangle(width,height) {
`Wthis`.width=width
`Wthis`.height=height
}
Rectangle.prototype.area=function() {
`Wreturn` `Wthis`.width*`Wthis`.height;
}
Rectangle.prototype.setWidth=function(width) {
`Wthis`.height=height;
}
`Wvar` r=`Wnew` Rectangle(`P5`, `P6`);
r.setWidth(`P10`);
r.area();
This can be recreated without using this by using a factory pattern and binding functions, as follows:
`Wfunction` Rectangle(width,height) {
`Wvar` o={};
o.width=width;
o.height=height;
o.area=(`Wfunction`(self) {
`Wreturn `self.width*self.height;
}).bind(o,o)
o.setWidth=(`Wfunction`(self,width) {
self.width=width;
}).bind(o,o)
o.setHeight=(`Wfunction`(self,height) {
self.height=height;
}).bind(o,o)
`Wreturn` o
}
var r=`Wnew` Rectangle(5,6);
r.setWidth(10);
r.area();
`LMISC`
[0] `Phttp://pastebin.com/zUpYzEFv` - @dtr/@sudo’s impromptu tutorial transcript on 7001
[1] `Phttp://``DDEAD URL`/fanstuff/hackmud/coding-info.html` code reference, including #db info
(ED NOTE: )
[2] dtr.man - This is dtr’s man script. This is a user’s script so be careful
[3] On linux, the script folder may be located in: ~/.config/hackmud or the game folder, eg hackmud/<name>/scripts/myscript.js. On windows, it’s C:\Users\USERNAME\AppData\Roaming\hackmud\INGAMENAME\scripts
[4] Predicates and Each functions from scripts.lib use function(key, value) { } function signature. People already familiar with JS: note that this is the opposite of built-in Array functions, like Array.prototype.forEach!
[5] `Phttps://github.com/HeatherSoron/hackmud_sample_scripts` - collection of sample scripts, curated by @soron
[6] `Phttps://docs.google.com/document/d/1MldDYVbUnB3Pq0KgkSOTtMNjqst1vdhn-mTydCZeTrs/edit`
---
Hopefully this got you started on script development for hackmud. If it did so, and you think it’s worth it, and you have enough GC, spare us an upgrade or two. Stay creative. Stay safe.