1 hour ago1 hr comment_207502 MDScript for Players: What I Learned Building Clickies======================================================Hi all, Seigheart here.I built the Primordial Keeper, the Primordial Archive and the Cartographer's Cipher, and along the way I ran into pretty much every wall MDScript has. So I've put together what I wish someone had handed me at the start. It's all taken from the official MD Script Manual on the forum and from scripts that actually run in game. Where I'm not sure about something, I'll say so.What It Is and Where It Goes----------------------------MDScript is a cut-down PHP that runs on the server whenever someone clicks your clicky. No classes, no $ variables. You get a fixed set of @ variables and that's it.Open your clicky with "edit own" and you'll see four fields: Title at the top, Content on the left, Script on the right, and Password at the bottom. Hit save&compile to apply your changes.Content holds HTML: text, images, forms. Only the part between <body> and </body> gets used, and newlines are ignored because it's HTML, so use <p> or <br>.Here's the thing that confuses everyone at first. If the Script box is empty, your Content shows as it is. The moment the Script box has anything in it, the Content disappears until your script does echo @content[0];.You can cut the Content box into pieces with this exact line:<!-- content separator -->The pieces become @content[0], @content[1] and so on, starting at 0. It has to say "content separator", not "content section". Then you can show different pieces under different conditions. Plenty of us just leave Content empty and print everything from the Script box with echo. That's what I do.One more thing: what you see in the editor right after save&compile can differ from what a normal visitor sees, and that includes storage. Always test as a visitor.Your First Script-----------------Every script needs a locations line:(locations 1_0x0_1)No quotes, no ; at the end. For several locations, separate them with commas and no spaces, e.g. (locations 1_0x0_1,1_1x1_1). You get 12 at most. Variables don't work in it.The editor shows the right line for your spot above the code field ("Add location setting: (locations X)"). Put it on the first line. The manual says "anywhere", but the error message says first line, so I go with the error message.For reference, 1_0x0_1 is the Gazebo of Equilibrium. I'm not 100% sure what each number in a coordinate means, so don't take my word on that.Watch out for this one: an "incorrect location" error can show up when the real mistake is somewhere else in your code entirely.Printing is easy. echo and print write HTML into the page:(locations 1_0x0_1) echo "<p>Hello, traveller!</p>";Variables are where it gets odd. You can't make your own. You get @va to @vz (26 general ones), plus @tpl, @temp and @log, which work the same way and just have friendlier names. Then there are the special ones: @input (form data), @content (Content pieces) and @storage (saved data).Even loop counters have to come from that set: for (@vi = 0; @vi < 4; @vi++) { ... }. foreach (@vp as @vk => @vh) works fine. Arrays work too, so one variable can hold a lot: @vu['colour'] = 'gold';.Forms and @input----------------Put a form in your Content box or echo one from your script:<form method="post" action=""> <button type="submit" name="go" value="A">Option A</button> </form>It posts back to the same clicky, and whatever was sent shows up in @input:if (isset(@input['go']) and @input['go'] == 'A') { echo "You picked A"; }$_POST isn't allowed. Always use @input.Now, two gotchas. First, field names. A field name containing a banned bit of text breaks the script. The forum example is a field with "val" in its name, which had to be renamed hidden_button.Second, and this one bit me constantly: the word "value" itself. The filter checks your whole script, strings included, and "value" contains "val". If you echo your buttons from the Script box, build the word in pieces:@vv = 'va' . 'lue'; echo "<button type='submit' name='go' " . @vv . "='A'>Option A</button>";HTML typed straight into the Content box isn't part of your script, so value="Yes" is fine there. The official tutorial does exactly that.Always check what comes in, because players can send anything. Use isset() first, trim() it, compare it to the exact things you expect, and wrap anything you print back in htmlspecialchars() or strip_tags(). For numbers, check it's only digits, e.g. strspn(@vx, '0123456789') == strlen(@vx).There's no JavaScript button handling here. Every button is just another post back to the server.Remembering Things------------------Storage with mds_storage()This is the main way to keep data:@vo = mds_storage('myname', 'toau');It gives you back a number. From then on, read and write @storage[@vo], and whatever's in there gets saved. The second part is the type:- totu: this object, this user- toau: this object, all users- aotu: all objects, this user- aoau: all objects, all users (kept apart per editor)The same name with a different type is a different storage. You can't read another editor's storage, and an empty storage gets deleted.Start it as an array the first time. If it ends up as a plain number, later writes fail with "Cannot use a scalar value as an array". This is the opener I use:if (!isset(@storage[@vo]) or is_scalar(@storage[@vo])) { @storage[@vo] = array(); }For one record per player inside a shared storage: @storage[@vo][uv('name')] = ...;I haven't tested the size limits myself, or what happens if two players save in the exact same instant. My guess is the last write wins, but I don't know for sure.The old mds_storage_load / mds_storage_read names reportedly get rejected because they contain "load" and "read". Just use mds_storage().store() and retrieve()store(@vh); in one item and retrieve(@vh); in another. It can hold whole arrays. The forum says it's one value per scripter and player, but I haven't checked that myself.Session DataThis is the temporary, fast option. mds_store_sessdata(value, box) (alias storetmp) saves, and mds_retrieve_sessdata(@holder, box) (alias retrievetmp) reads.You get 10 boxes (0 to 9) per player, and each editor has their own set. Strings only, max 250 characters, so serialize() arrays first. It lasts until the player logs out and is shared by all your items.KeysKeys are yes/no flags on the player:mds_give_rpcq_keys('zq77-door'); // give mds_has_rpcq_keys('zq77-a,zq77-b') // true only if the player has ALL of them mds_take_rpcq_keys('zq77-door'); // take awayThey're comma-separated text, no arrays. And they're global: any script checking the same name sees the same key. The game saves them as rpcq-<name>. So pick a unique, secret prefix, or someone will collide with you or fake your keys.Keys aren't for keeping numbers. Use storage for counts.Where keys really shine is linking two clickies. That's how I connected the Primordial Keeper and the Cartographer's Cipher, and it's how you'd build a quest that starts at one clicky and ends at another. One thing I'm not sure of: whether a key you give in this click can be checked in the same click. I build around it so it doesn't matter.Reading the Player------------------uv('name') reads facts about whoever is clicking. It's read only. The names I know of:id, ve, vp, xpl, maxve, maxvp, land, alliance, age (active days), loyalty, honor, name, mp, heads, won, lost, avatar_level (0 none, 1 B/W, 2 gold access), avatar, xp, kills, illusion, location (coordinates text), stored_heat, and since 2013: tag, description, principles, landscore.Funny one: "description" contains a banned piece, so you have to write uv('descri' . 'ption').Land numbers are 1 No Homeland, 2 Marind Bell, 3 Loreroot, 4 Golemus, 5 Necrovion, 6 Underground, 7 Archives, 8 No Mans Land, 9 unknown, 10 Lands of the East.date('z') gives you the MD day (day of the year). Server time zone is Europe/Helsinki.A few other checks on the player:- mds_has_item('Silver coin'): how many of that item they hold. Coins and resources count.- mds_is_itc('ITC...MDI'): checks an item transfer code and gives back its details, or false.- mds_is_ctc('CTC...MDC'): checks a creature transfer code and gives back the creature's details, or false.- mds_location_visits('current'): how often they've been here. Handy for hiding spoilers.- mds_player_heat(), mds_get_resource_count(), mds_current_weather().Fair warning: you can only check items. Nothing documented lets a script give or take items, creatures or quests. Quests get built out of keys and storage.Examples--------All six of these pass a PHP syntax check and a banned-word scan. Swap 1_0x0_1 for your own location, and change the zq77- key prefix and the storage names to your own. The real test is still save&compile in game.Hello and a Click Counter(locations 1_0x0_1) // Hello and a shared counter for this clicky. @va = uv('name'); @vo = mds_storage('myhellocount', 'toau'); if (!isset(@storage[@vo]) or is_scalar(@storage[@vo])) { @storage[@vo] = array(0); } @storage[@vo][0]++; echo "<p>Hello, " . htmlspecialchars(@va) . "!</p>"; echo "<p>This clicky has been clicked " . @storage[@vo][0] . " times.</p>";A Button Menu(locations 1_0x0_1) // A menu of buttons. Each button posts go=A, go=B or go=C. @vv = 'va' . 'lue'; echo "<form method='post' action=''>"; echo "<button type='submit' name='go' " . @vv . "='A'>Lore</button> "; echo "<button type='submit' name='go' " . @vv . "='B'>Map tip</button> "; echo "<button type='submit' name='go' " . @vv . "='C'>Who am I?</button>"; echo "</form>"; @vg = isset(@input['go']) ? trim(@input['go']) : ''; if (@vg == 'A') { echo "<p>Long ago, a gazebo stood here in perfect balance.</p>"; } elseif (@vg == 'B') { echo "<p>Look for clickies near the stairs.</p>"; } elseif (@vg == 'C') { echo "<p>You are " . htmlspecialchars(uv('name')) . ", with " . uv('age') . " active days.</p>"; }A Once-a-Day Greeting(locations 1_0x0_1) // Once per MD day per player. date('z') is the day of the year. @va = uv('name'); @vd = date('Y') . '-' . date('z'); @vo = mds_storage('mydailygreet', 'toau'); if (!isset(@storage[@vo]) or is_scalar(@storage[@vo])) { @storage[@vo] = array(); } if (isset(@storage[@vo][@va]) and @storage[@vo][@va] == @vd) { echo "<p>You have had your greeting today. Come back tomorrow!</p>"; } else { @storage[@vo][@va] = @vd; echo "<p>Good day, " . htmlspecialchars(@va) . "! Here is your daily blessing.</p>"; }A Small Quest with Keys(locations 1_0x0_1) // A three step quest kept in keys. Pick your own odd prefix so nobody guesses it. @vv = 'va' . 'lue'; @vg = isset(@input['go']) ? trim(@input['go']) : ''; if (mds_has_rpcq_keys('zq77-step2')) { echo "<p>The quest is complete. Well done!</p>"; } elseif (mds_has_rpcq_keys('zq77-step1')) { if (mds_has_item('Silver coin') >= 1) { mds_give_rpcq_keys('zq77-step2'); echo "<p>You show the silver coin. The quest is complete!</p>"; } else { echo "<p>Step 2: come back holding a Silver coin.</p>"; } } elseif (@vg == 'S') { mds_give_rpcq_keys('zq77-step1'); echo "<p>Quest started. Step 2: come back holding a Silver coin.</p>"; } else { echo "<form method='post' action=''><p>A stranger asks for help.</p>"; echo "<button type='submit' name='go' " . @vv . "='S'>Accept the quest</button></form>"; }A Riddle Door with a Typed Answer(locations 1_0x0_1) // A riddle door. The form field is named 'guess' (a field name holding v-a-l fails). @vv = 'va' . 'lue'; if (mds_has_rpcq_keys('zq77-door')) { echo "<p>The door is unlocked for you.</p>"; } else { @vg = isset(@input['guess']) ? strtolower(trim(strip_tags(@input['guess']))) : ''; if (@vg == 'echo') { mds_give_rpcq_keys('zq77-door'); echo "<p>Correct! The door swings wide.</p>"; } else { if (@vg != '') { echo "<p>Wrong. Think again.</p>"; } echo "<form method='post' action=''>"; echo "<p>I speak without a mouth and hear without ears. What am I?</p>"; echo "<input type='te" . "xt' name='guess' maxlength='30'> "; echo "<button type='submit' name='go' " . @vv . "='1'>Answer</button></form>"; } }Notice the 'te' . 'xt'. "Text" is on my cautious list below.Content Pieces (from the Official Beginner Tutorial)Content box:<form method="post"><input type="submit" name="wizard" value="Yes"/></form><!-- content separator --><p>Then go to the next room.</p>Script box:(locations 1_0x0_1) // Content box holds: a Yes button, then the separator, then a reply. echo @content[0]; if (isset(@input['wizard']) and @input['wizard'] == 'Yes') { echo @content[1]; }The Word Filter---------------This is the part that cost me the most time. The filter is a plain text check over your whole script: code, strings and comments. It doesn't care about grammar. If a banned piece shows up anywhere, even buried inside a longer word, you get one of these: [STRICT RESTRICTION], [GENERAL RESTRICTION], [LANGUAGE RESTRICTION] (function not supported) or [SEQUENCE RESTRICTION] (banned character sequence).While building my clickies it caught "catches", "gradient" and "Herald", and a lot more. Here's what I know is banned, from the manual, Kaya's 2013 tests and errors I've hit in game:script, extend, val, split, parse, open, mod, ini, ext, try, error, call, load, read, INF, is_a, T_AS, T_SR, E_PARSE, sprintf, key_exists, rename, catch, object, gradient, era, new, use, pass, stat, move, sid, die, true, and three dots in a row (...).Also not allowed: $ variables, $_POST, var_dump.Then there's my cautious list. Some are known PHP danger words and some have caused me trouble. I haven't confirmed every one of them is actually banned, but I avoid them all to be safe:http, text, value, empty, null, break, save, session, var, safe, file, exit, global, function, include, require, eval, exec, system, shell, class, static, header, cookie, server, request, onclick, onload, javascript, iframe, alert, document, window, import, join, link, and __.The fix is to split the word with . (which joins two strings):echo "ex" . "tend"; @vv = 'va' . 'lue'; @vg = "linear-gra" . "di" . "ent(";For three dots, put spaces between them: . . .These are the everyday words that tripped me up, what's hiding in them, and what I did instead:- valid, invalid, value (val): 'va' . 'lid'- gradient (gradient, die): "linear-gra" . "di" . "ent("- Herald, Seraph, Hourglass Seraph (era): 'Her' . 'ald'- already, ready, spread, Stonetread (read): 'Stonetre' . 'ad'- next (ext): reword it ("the day rolls over in")- shining (ini): reword- stately, status (stat): reword- catches (catch): reword- entry, Try (try): reword- remove (move): reword- side (sid): reword- compass, password (pass): reword- used (use): reword- moderation (mod): reword (I went with "warden")- object-fit in CSS (object): 'obj' . 'ect-fit'- justify-content in CSS (content): avoid it- description in uv (script): 'descri' . 'ption'- a true argument (true): pass 1 instead- array_values (val): loop over the array instead- is_array (is_a): use is_scalar- next(), str_split, preg_split (ext, split): not usable- page load, new day (load, new): rewordImage file names and CSS count too. A picture called "mirrorseraph.gif" will trip "era".The Functions You Can Use-------------------------MD Functions- mds_has_rpcq_keys('a,b'): True if the player has ALL the keys.- mds_give_rpcq_keys('a,b'): Gives the keys.- mds_take_rpcq_keys('a,b'): Takes the keys.- uv('name') (also mds_read_uvars): Reads a player fact (see Reading the Player).- mds_storage(name, type): Opens or creates storage; gives the index into @storage.- mds_storage_load, add, read: Old storage names; load and read reportedly blocked, not 100% sure.- mds_store_sessdata(value, box) / storetmp: Saves text (max 250) in session box 0 to 9.- mds_retrieve_sessdata(@holder, box) / retrievetmp: Reads a session box into a variable.- store(@v) / retrieve(@v): Simple keep and fetch across items.- mds_template(template, data, return, columns, td_attributes): Fills [[name]] placeholders; a list of records gives a table.- debug(@v, 'label'): Shows a coloured dump box (the manual's mds_debug)- mds_no_warnings(): Hides MDScript warnings only.- mds_has_item('name'): How many of an item the player has.- mds_is_itc(code): Checks an item code (pass only the ITC...MDI part); id, uid, playername, name, creationdate, transferdate, tag, or false.- mds_is_ctc(code): Checks a creature code (CTC...MDC part); id, uid, playername, cid, tokens, dateBorn, transfercount, tradevalue, customName, vitality, level, experience, battlesWon, age, or false.- mds_creature_icon(id, attrs): Prints a creature picture (creature ID, not the code)- mds_player_avatar(name, attrs): Prints an avatar; max 5 per script.- mds_player_heat(): The player's active heat.- mdsi_takeheat(amount): Moves heat from the player into the item; asking for more than they have takes all and keeps nothing.- mdsi_item_heat(): ['current' => total, 'players' => [name => heat]].- mds_location_visits(loc or 'current'): How many times the player has visited.- mds_check_group_location(players, coords): How many of those players are online there.- mds_current_weather(): E.g. "heavyrain", or false.- mds_get_resource_count(): [resource => count].mds_is_atc was only ever wished for on the forum. As far as I know it doesn't exist.Allowed PHP Functions (210)Mur's original list has 236. Kaya found that 26 of them fail because their names contain banned pieces, which leaves these 210:- Arrays: array, array_change_key_case, array_chunk, array_combine, array_diff, array_diff_assoc, array_diff_key, array_fill, array_fill_keys, array_flip, array_intersect, array_intersect_key, array_keys, array_merge, array_merge_recursive, array_multisort, array_pad, array_pop, array_product, array_push, array_rand, array_replace, array_replace_recursive, array_reverse, array_search, array_shift, array_slice, array_splice, array_sum, array_unique, array_unshift, arsort, asort, count, current, each, end, in_array, key, krsort, ksort, list, natcasesort, natsort, pos, prev, range, reset, rsort, shuffle, sizeof, sort, uasort- Text: base64_decode, base64_encode, chop, chr, count_chars, crc32, crypt, echo, explode, get_html_translation_table, hash, htmlspecialchars, htmlspecialchars_decode, implode, lcfirst, md5, number_format, ord, print, printf, serialize, str_ireplace, str_pad, str_repeat, str_replace, str_rot13, str_shuffle, str_word_count, strcasecmp, strchr, strcmp, strcspn, strip_tags, stripos, stripslashes, stristr, strlen, strnatcasecmp, strnatcmp, strncasecmp, strncmp, strpbrk, strpos, strrchr, strrev, strrpos, strspn, strstr, strtok, strtolower, strtoupper, strtr, substr, substr_compare, substr_count, substr_replace, trim, ucfirst, ucwords, unserialize, url, urldecode, urlencode, vprintf, wordwrap- Dates and times: date, date_add, date_create, date_diff, date_format, date_offset_get, date_sub, date_sunrise, date_sunset, date_timestamp_get, date_timezone_get, getdate, gettimeofday, gmdate, gmmktime, gmstrftime, idate, localtime, microtime, mktime, strftime, strptime, strtotime, time, timezone_location_get, timezone_name_get, timezone_offset_get, timezone_transitions_get- Patterns: preg_filter, preg_match, preg_match_all, preg_quote, preg_replace- Type checks: is_binary, is_bool, is_double, is_float, is_int, is_integer, is_nan, is_null, is_numeric, is_scalar, is_string, isset, unset- Maths and other: abs, acos, acosh, asin, asinh, atan, atan2, atanh, base_convert, bindec, ceil, cos, cosh, decbin, dechex, decoct, deg2rad, exp, expm1, floor, getrandmax, hexdec, hypot, log, log10, log1p, max, min, mt_getrandmax, mt_rand, octdec, pi, pow, rad2deg, rand, round, sin, sinh, sqrt, srand, tan, tanh- Old encryption: mcrypt_cbc, mcrypt_cfb, mcrypt_ecb, mcrypt_ofburl is on the list but isn't a real PHP function. The basic language pieces like if, else, elseif, for, foreach, isset, array, list, echo and print all work.These are on the list but don't work: array_count_values, array_intersect_assoc, array_values, chunk_split, date_interval_format, date_modify, date_parse, date_sun_info, fmod, the three geoip_ functions, is_array, is_finite, is_infinite, lcg_value, mt_srand, next, preg_last_error, preg_replace_callback, preg_split, similar_text, str_split, timezone_open, vfprintf, vsprintf. Also array_key_exists (removed) and var_dump (disabled).Anything not on the list (for example ctype_digit) gives you a [LANGUAGE RESTRICTION].Limits, Tips and Testing------------------------The limits I know of: 12 locations per script, 10 session boxes of 250 characters, and 5 mds_player_avatar calls per script. A member mentioned about 5 seconds of run time and a memory cap, but I haven't seen that confirmed officially. There's no official figure for maximum script size or storage size.To find bugs, print values: print "V=" . @va; or debug(@va, 'va');. If a fatal error breaks the page, a blue "edit in case of error" link lets you fix it.Design for the popup. The live clicky window is small, about 535 px wide and 165 px tall before it scrolls.Build everything server-side with buttons. Each click is a fresh run, so keep state in storage or keys.Know the staff rules. Don't copy or improve game or MD Shop features, no chats or messaging, don't show pictures of places players can't reach, "alternate reality" items need 70+ active days, and there's zero tolerance for abusive or tricking scripts. And never send players to the wrong internal page; staff warn that can get you banned.If you have PHP on your computer, you can check grammar like a missing ; or unmatched brackets. Remove the (locations ...) line, change every @ to $, add <?php at the top, then run php -l. Line numbers will be off by one. It only catches syntax, though. It won't catch banned words, banned functions or wrong variable names, so search your script for the words above too. The real test is save&compile and then clicking it as a visitor.The Official Docs------------------ MD Script Manual (forum section): https://forum.magicduel.com/forum/168-md-script-manual/- Step-by-step introduction for absolute beginners: https://forum.magicduel.com/topic/16837-a-step-by-step-introduction-to-mdscript-for-absolute-beginners/- Variables: https://forum.magicduel.com/topic/5494-variables/- Location setting: https://forum.magicduel.com/topic/6799-location-setting/- Content sections: https://forum.magicduel.com/topic/5495-managing-content-sections/- Storing values: https://forum.magicduel.com/topic/5492-how-to-store-valuesvariables-using-mdscript/- Key control: https://forum.magicduel.com/topic/5498-key-control/- User variables: https://forum.magicduel.com/topic/5499-user-variables/- Restrictions and errors: https://forum.magicduel.com/topic/5493-restrictions-and-errors/- PHP functions available: https://forum.magicduel.com/topic/5500-php-functions-availble-in-mdscript/- Templates: https://forum.magicduel.com/topic/5496-templates/- Debugging: https://forum.magicduel.com/topic/5497-debugging-your-code/- Index of basic functions: https://forum.magicduel.com/topic/14268-index-of-basic-mdscript-functions-links/- Content rules: https://forum.magicduel.com/topic/7137-new-rules-regarding-md-script-and-item-customizations/Skip the "Old MD Script Language Reference". It's out of date.Happy scripting, and if you build something, show me!Seigheart Edited 28 minutes ago28 min by Seigheart Report
46 minutes ago46 min Root Admin comment_207503 Looks like a good start! Can I suggest you put the code into the code blocks to make it easier to read? Report
42 minutes ago42 min Author comment_207504 4 minutes ago, Chewett said:Looks like a good start! Can I suggest you put the code into the code blocks to make it easier to read?Done! Thank you! Edited 29 minutes ago29 min by Seigheart Report
Join the conversation
You can post now and register later. If you have an account, sign in now to post with your account.