Cookies
A cookie is a named value. Write #<name> anywhere in a script line and the value is
put in its place before the line runs. Some cookies are yours: SET gold_owed, 500 stores one, and it
is kept in the hero's own file so it survives between sessions. The rest are stock cookies the game
computes on the spot: the hero's level, the time of day, the monster just selected.
Each section below ends with a worked example, and the last section is a full walkthrough of the
DevTable files, the one part of the cookie system that needs more than a line to explain. The
examples are made up: every item, monster, map and table number is a placeholder.
Rules
- Expansion is one pass from left to right. An inserted value is never expanded again, and names do not nest.
*#<name>reads the cookie, then reads the cookie named by that value (two levels at most).- Stock names are not case-sensitive. Your own names are stored as written.
- Setting a cookie to nothing deletes it. Nothing is stored unless a hero is in play.
- Setting a stock cookie that is marked computed below writes a line to the file, but reads keep returning the computed value. Setting one marked action does something in the game.
cookieProtectioninconfig.iniadds a hash to stored cookies (1 for names that start withsecure, 2 for all). ReBreather writes the hashes but does not yet check them.- The original's list also names
num.hostCookieFaults; it never answers, on either client.
Example: one pass, and the pointer
SET stat, num.hostGP ' You carry *#<stat> gold. SET stat, num.hostLevel ' And you are level *#<stat>. SET n, 3 ' Slot #<n> holds item #<num.hostEquipId#<n>>.
The first two lines show the pointer: stat holds the name of a stock cookie, and
*#<stat> follows it to the value. The last line is the mistake to avoid. Because expansion is
one pass and names do not nest, #<num.hostEquipId#<n>> is not read as slot 3. Build the
name first and follow it instead:
SET n, 3 SET slot, num.hostEquipId#<n> ' Slot #<n> holds item *#<slot>.
The hero
| Cookie | Value |
|---|---|
num.hostClass | Class number. Action: setting it changes the hero's class (used with HIDDEN_CLASS). |
num.hostLevel, num.hostXP, num.hostMaxXP, num.hostTotalXP, num.hostTNL | Level and experience; TNL is the experience still needed for the next level. |
num.hostHP, num.hostMaxHP, num.hostMP, num.hostMaxMP | Current and maximum HP and MP. |
num.hostSTR, WIS, STA, AGI, DEX, num.hostAttack, num.hostDefense | Abilities and the attack and defence totals. |
num.hostGP, num.hostPP, num.hostTotalPP | Gold, unspent training points, lifetime training points. |
num.hostHandSkill3, num.hostElementSkill3, num.hostHandPP3, num.hostElementPP3 | Level and points in hand 3 or element 3. |
num.hostHandRatio, num.hostMagicRatio | The class's split between weapons and spells. |
num.hostX, num.hostY | Position in the scene, 0 to 100. |
num.hostAge, num.hostTotalAge, num.hostBirthDay, Month, Year | Minutes played by this hero and on this computer, and the hero's birth date. |
num.hostEquipId0 ... num.hostEquipId13 | The item worn in each slot. |
num.item12 | How many of item 12 the hero carries. |
num.hostKills, num.hostDeaths, num.hostPKs, num.hostHuntSkill, num.hostHaloPoints, num.hostRating | Tallies and ratings. |
num.hostPets, num.hostPetLevel, num.hostPetId, num.hostHasPet40 | Pets owned, the highest pet level, the pet out in the scene, and pets of monster 40 owned. |
num.hostBravery, num.hostAggressiveness | The hero's own settings. |
str.name, str.uniform, str.soul | The hero's name, guild and serial. |
g.num, g.<key> | Gender 0 to 3 (action: setting it changes gender), and any key of the hero's gender section in gender.ini, so #<g.heshe> can say he or she. |
Example: a fletcher who looks you over
SCENE 130, shop,, "The Fletcher" ACTOR 1, Wren, fletcher, 0, 50, 70 ' #<str.name>, is it? Level #<num.hostLevel>, and #<g.heshe> looks it. SET arrows, #<num.item12> COMPARE #<arrows>, 20 IF>=, @stocked ' Only #<arrows> arrows? That's no way to walk the woods. Take these. GIVE I12.10 GOTO @health @stocked ' Well stocked, I see. @health SET lost, #<num.hostMaxHP> SUB lost, #<num.hostHP> COMPARE #<lost>, 0 IF=, @fine ' And you've lost #<lost> of your #<num.hostMaxHP> health. Sit a moment. GIVE H#<lost> END @fine ' Not a scratch on you. Off you go. END
Every stock value is read at the moment the line runs, so the arrow count is copied into
arrows before the test and the speech agree on one number. The heal is sized from the difference
between maximum and current HP, and since values are expanded before the command runs, GIVE
H#<lost> becomes a plain GIVE H37 by the time the engine sees it.
Kills and answers
| Cookie | Value |
|---|---|
KM.40, KM.0 | Kills of monster 40, and all kills. |
KM.undead | Kills of every monster whose alignment word is undead. Counted as the kills happen, so a quest for ten of a kind is one IF. |
lastAsk | The hero's last answer to ASK. |
Example: a bounty on the walking dead
SCENE 140, chapel,, "The Sexton" ACTOR 1, Sexton, sexton, 0, 50, 70 IF T30, @paid SET left, 10 SUB left, #<KM.undead> COMPARE #<left>, 0 IF<=, @claim ' Ten of the walking dead, I said. #<left> more and the village sleeps easy. ' What do they call you, by the way? ASK 15 STRCMP #<lastAsk>, #<str.name> IF=, @honest ' #<lastAsk>, is it? Your mother must have been a cruel woman. END @honest ' That's what the gravestone will say, then. END @claim ' Ten, and more besides. Here's your due. GIVE G300 GIVE T30 END @paid ' Peaceful lately, isn't it? Thanks to you. END
KM.undead counts every kill of a monster whose alignment word is undead, so the sexton
does not need to know which monsters those are. The count is the hero's lifetime tally, which is why the reward
is locked behind token 30 rather than the count itself. The ASK stores whatever the player types
in lastAsk, and STRCMP against str.name ignores case.
The selected item, monster and spell
Set item.id, monster.id or spell.id and the rest describe that row of
the table. The game sets item.id itself at gift, click and attack events.
| Cookie | Value |
|---|---|
item.name, class, price, level, element, attack, defense, hp, mp, ap, desc, fp | The item's columns (ap is the ability points it grants, fp the find chance). |
item.goldGiven, item.trophyGiven | What the last gift to an actor contained. |
monster.name, hp, mp, attack, defense, str, sta, agi, dex, wis, level, element, expPts, gold, alignment, skin | The monster's columns. |
spell.name, pp, element, damage, level, price | The spell's columns (price is the MP to cast). |
Example: a scholar who reads the tables
SCENE 150, library,, "The Bestiary" ACTOR 1, Scholar, scholar, 0, 50, 70 SET monster.id, 44 ' The #<monster.name>: #<monster.hp> hit points, strikes for #<monster.attack>, and worth #<monster.gold> gold to the hunter. SET spell.id, 14 ' Against it I would use #<spell.name>. It costs #<spell.price> magic to cast. IF S14, @knows ' You don't know it? Then be careful. END @knows ' Ah, you already know it. Good. END @eventActorGive1 COMPARE #<item.goldGiven>, 0 IF>, @gold ' A #<item.name>? I write about these; I don't collect them. It says here: #<item.desc> END @gold ' #<item.goldGiven> gold for the library. Very kind. END
The scholar sets monster.id and spell.id by hand and then reads whichever columns
the sentence needs. In the gift handler nothing needs setting: the game has already put the given item in
item.id, so item.name and item.desc describe it at once. Gold arrives as a
gift of item 0 with the amount in item.goldGiven, so that is tested first.
The trophy bag
| Cookie | Value |
|---|---|
num.Trophy7, num.TrophyBagRoom7 | How many of trophy 7 the hero holds, and how many more would fit. |
num.TrophyBagWidth, num.TrophyBagHeight | Action: setting them resizes the bag. |
num.TrophyBagSlots, num.TrophyBagEmptySlots, num.TrophyBagSlotsInUse | Slot counts (action: setting SlotsInUse empties the bag). |
num.TrophyBagOpen | 1 while the bag window is open. Action: 1 shows it, 0 hides it. |
num.TrophyIdInSlot3, num.TrophyCountInSlot3 | What is in slot 3 and how many. Action: setting them fills the slot. |
num.TrophyId, then num.TrophyStackSize, StackCost, Probability, Token, Flags | Select a trophy definition and read its columns. |
Example: a tanner who checks the bag first
SCENE 160, tannery,, "The Tanner" ACTOR 1, Tanner, tanner, 0, 50, 70 ' Pelts. I pay four gold the pelt, or I'll trade two of mine for a bear hide. ' You've #<num.Trophy7> wolf pelts on you, and room for #<num.TrophyBagRoom7> more. MENU "Sell all my pelts=@sell", "Trade a bear hide=@trade", "Show me the bag=@bag", "Leave=@bye" GOTO @bye @sell COMPARE #<num.Trophy7>, 0 IF=, @none SET pay, #<num.Trophy7> MUL pay, 4 TAKE Z7.#<num.Trophy7> GIVE G#<pay> ' #<pay> gold. Pleasure. END @trade IF -Z8, @noHide COMPARE #<num.TrophyBagRoom7>, 2 IF<, @full TAKE Z8 GIVE Z7.2 ' Two pelts for a hide. Fair. END @bag SET num.TrophyBagOpen, 1 ' Have a look. END @none ' You've no pelts to sell. END @noHide ' No bear hide, no trade. END @full ' Your bag won't take two more. Make room and come back. END @bye END
num.Trophy7 is read into the TAKE line directly, so the tanner takes exactly the
pelts the hero has, and the sum is worked out in a cookie first because GIVE takes a number, not a
calculation. num.TrophyBagRoom7 answers the question a trade has to ask before it gives: would two
more of trophy 7 fit? Setting num.TrophyBagOpen to 1 is one of the action cookies and opens
the bag window.
Time, server, world and scene
| Cookie | Value |
|---|---|
num.timeHour, Minute, Day, Month, Year, DoW, num.timeNow | The player's clock (DoW 1 is Sunday; timeNow is seconds since 1970). |
num.serverTimeHour ... num.serverTimeNow | The server's clock, in the player's time zone. |
num.countDown, num.timerLeft2, num.timerLength2 | Seconds left on the countdown; time left and length of timer 2 in milliseconds. |
str.serverName, num.ruleMaxPlayers, NoPK, AllPK, NoCheat, NoMod, NoPets, Ladder, ... | The server and its rules. |
num.engineVersion, num.wosVersion | 2817 in ReBreather under either name (the original answered 2711, and only to the second), so a script can tell which client it is on. |
str.worldName, num.mapNum, str.mapName, num.mapFlags, num.sceneMapFlags | The world and the current map. num.mapFlags is the maps table's column; num.sceneMapFlags is what this scene's FLAGS set. |
num.closestLink, num.closestLinkStyle | The nearest link (-1 when none) and its style. |
scene.id, scene.bkgnd, scene.fx, scene.weather | The current scene. |
num.peopleInScene, num.peopleInParty, str.actorName3 | Head counts and actor names. |
num.isPKAttack | 1 during a player-versus-player fight. |
num.isTactics | 1 in the original's Tactics mode; always 0 in ReBreather. |
num.holidayId, str.holidayName, str.holidayGreeting | Today's holiday from the player's calendar; the ids, names and greetings are listed below. |
event.actor, event.monsterIndex, event.x, event.y, event.missionId | Set by the game when an event label fires. |
arg0 ... arg9, numArgs | Set by CALL and RETURN. |
Holidays
The calendar is checked in this order and the first match wins: Easter, the hero's birthday (the birth month and day, in a later year), the weekday holidays, then the fixed dates. On any other day the id is 0, the name Not a Holiday and the greeting Get back to work. The greeting is Happy and the name, except Merry Christmas on the 24th and 25th, Happy New Year on the 31st and the 1st and Happy April Fools Day. Two oddities of the original's own table are kept, since scripts were written against them: New Year's Day carries id 7, the same as New Year's Eve, so id 8 never appears and 1 January reports New Year's Eve; and Groundhog Day falls on 8 February, not the 2nd.
| Id | Holiday | When |
|---|---|---|
| 1 | Birthday | the hero's birth month and day |
| 2 | Easter | computed for the year |
| 3 | Thanksgiving | the fourth Thursday of November |
| 4, 5, 6 | Christmas Eve, Christmas, Boxing Day | 24, 25 and 26 December |
| 7 | New Year's Eve, New Year's Day | 31 December and 1 January (both id 7, both reported as New Year's Eve) |
| 9 | Groundhog Day | 8 February |
| 10 | Valentine's Day | 14 February |
| 11 | St. Patrick's Day | 17 March |
| 12 | April Fools | 1 April |
| 13 | Earth Day | 22 April |
| 14 | Cinco de Mayo | 5 May |
| 15 | Mothers Day | the second Sunday of May |
| 16 | Memorial Day | the last Monday of May |
| 17 | Fathers Day | the third Sunday of June |
| 18 | Canada Day | 1 July |
| 19 | Independence Day | 4 July |
| 20 | Labor Day | the first Monday of September |
| 21 | Columbus Day | the second Monday of October |
| 22 | Halloween | 31 October |
| 23 | Veteran's Day | 11 November |
Example: a gatekeeper with a clock and a calendar
SCENE 170, gate,, "The Town Gate" ACTOR 1, Watchman, guard, 0, 50, 70 COMPARE #<num.holidayId>, 0 IF=, @plainDay ' #<str.holidayGreeting> It's #<str.holidayName>; the gate's open all day. GOTO @company @plainDay COMPARE #<num.timeHour>, 6 IF<, @night COMPARE #<num.timeHour>, 20 IF>=, @night ' Fine day for it. Gate's open. GOTO @company @night ' It's gone #<num.timeHour> o'clock, %2. The gate is shut till six. IF -T9, @turnedAway ' But the captain's token opens it. Go on. @company COMPARE #<num.peopleInScene>, 1 IF=, @alone ' And bring your #<num.peopleInScene> friends through quickly. END @alone ' Travelling alone? Brave, on the #<str.mapName> road. END @turnedAway ' Come back at six. GOTO EXIT
On an ordinary day num.holidayId is 0 while the name reads Not a Holiday, so test the
id, not the name. The clock is the player's own, so the gate shuts by their local evening. A script
that wants one clock for everyone uses num.serverTimeHour instead.
Example: telling the clients apart
COMPARE #<num.engineVersion>, 2817 IF<, @original N: This world uses ReBreather's extended behaviour. GOTO @on @original N: Running on the original client; some scenes will look different. @on
Your own tables
A world can ship up to a hundred tables of its own, DevTable00.txt to
DevTable99.txt, in the world folder next to quest.txt. They are read once, when the
world loads, and reached through three cookies. The idea is to keep data out of the script: a price list, a
bank of rumours, a bounty board or a schedule lives in a table, and one short scene reads whichever row it
needs. Changing the data then means editing a text file, not a script.
The file
Each line is one row. Cells are separated by commas or by runs of spaces, and the first cell is the row's key. The rules are those of the other data tables:
- The file name takes two digits:
DevTable07.txt, neverDevTable7.txt. The case of the name does not matter. - A semicolon outside quotes ends the line, so a table may carry end-of-line comments, which scripts may not. Blank lines are skipped.
- A cell that contains spaces, commas or a semicolon goes in double quotes. The quotes are not stored. Two
quotes inside a quoted cell,
"", stand for one quote mark. - A backslash keeps the next character as it is, so
\,is a comma inside an unquoted cell. - Two commas in a row make an empty cell. A run of spaces does not.
- The key is the first cell with its spaces trimmed. If two rows share a key, the later one replaces the earlier.
- Any key, row or column that does not exist reads as an empty value, not an error.
; DevTable01.txt -- what the pedlar sells on each map ; key = map number, then item, price, patter 1, 12, 40, "Fresh from the mill. No weevils, I promise." 2, 15, 65, "Rope. Everyone needs rope." ; the mountain road 3, 40, 120, "A ""lucky"" charm. Lucky for me, anyway." 4, 41,, "Not for sale. Don't ask."
Row 4 has an empty third cell, written as two commas. Row 3's patter contains a quoted word. The comments after the semicolons are dropped when the file is read.
The three cookies
| Cookie | Value |
|---|---|
num.DevTableId | Action: which table the next reads use, 0 to 99. Set it once; it stays until changed. |
str.DevTableRowId | Action: the key of the row to read, matched exactly as written in the file, including case. |
str.DevTableCOL2 | Column 2 of that row. Column 0 is the key itself. A missing table, row or column gives an empty value. |
Column numbers count from 0, and 0 is the key column, so the item in the pedlar's table above is
str.DevTableCOL1, the price str.DevTableCOL2 and the patter
str.DevTableCOL3.
Example: one pedlar scene for four maps
Links on four maps all lead to scene 60. The scene reads the row whose key is the current map number, so the pedlar sells something different on each.
SCENE 60, road,, "The Pedlar" ACTOR 1, Pedlar, pedlar, 0, 50, 70 SET num.DevTableId, 1 SET str.DevTableRowId, #<num.mapNum> SET_LEN n, #<str.DevTableCOL1> COMPARE #<n>, 0 IF=, @nothingHere SET item.id, #<str.DevTableCOL1> ' #<str.DevTableCOL3> SET_LEN n, #<str.DevTableCOL2> COMPARE #<n>, 0 IF=, @notForSale MENU "Buy the #<item.name> for #<str.DevTableCOL2> gold=@buy", "No thanks=@bye" GOTO @bye @buy IF -G#<str.DevTableCOL2>, @poor TAKE G#<str.DevTableCOL2> GIVE I#<str.DevTableCOL1> ' Pleasure doing business, %2. END @poor ' Come back when your purse is heavier. END @notForSale ' I said don't ask. END @nothingHere ' Nothing for sale on this road. Try the next one. END @bye END
Two length checks do the error handling. The first catches a map that has no row at all, since a missing
row reads as empty in every column. The second catches row 4's empty price, which must be caught before it
reaches IF -G, because a condition with no number after the letter is not a valid test. Setting
item.id from the table lets the menu name the goods with item.name. Adding a fifth map
is a new line in the table; the scene does not change.
Example: a bank of rumours
; DevTable02.txt -- tavern rumours, keys 1 to 5 1, "They say the miller has not paid his tithe in a year." 2, "Wolves on the north road. Big ones." 3, "The sexton buries more than he digs up, if you ask me." 4, "A pedlar came through selling lucky charms. Lucky for him." 5, "Old Mara is looking for marshlight again. Pays well."
@rumour SET num.DevTableId, 2 SET str.DevTableRowId, %R5 ' #<str.DevTableCOL1> GOTO @menu
The value of a SET is cookie-expanded and then run through the % codes, so
%R5 rolls 1 to 5 and the key becomes one of the five numbers. A bigger bank is more lines in the
file and a bigger number after the R.
Example: a row keyed by the player's answer
; DevTable03.txt -- what the gossip knows, keyed by a lower-case word mill, "Burned down twice. Nobody says why." wolves, "Worse since the ranger left. Someone should look into it." mayor, "Bought his chain of office. Didn't earn it." mara, "Lives by the marsh. Knows more about herbs than is natural."
SCENE 180, square,, "The Gossip" ACTOR 1, Gossip, villager, 0, 50, 70 ' Ask me about anyone or anything. One word. ASK 20 STRLWR key, #<lastAsk> SET num.DevTableId, 3 SET str.DevTableRowId, #<key> SET_LEN n, #<str.DevTableCOL1> COMPARE #<n>, 0 IF=, @unknown ' #<str.DevTableCOL1> END @unknown ' Never heard of it. Ask me something else. END
Row keys are matched exactly, so the table keeps every key in lower case and the script lowers the answer
with STRLWR before using it. A word the table does not know reads as an empty column, which the
length test turns into a polite refusal. The same scene can answer a hundred questions, and adding one is a
line in the table.
Example: walking numbered rows
When the keys are 1, 2, 3 and so on, a script can step through the whole table without knowing how long it is: it stops at the first key that reads back empty.
; DevTable04.txt -- the bounty board: monster, how many, name 1, 40, 5, "wolves" 2, 44, 3, "bog hounds" 3, 51, 1, "the ogre of the mill"
SCENE 190, square,, "The Bounty Board" N: The board lists what the town will pay for. SET num.DevTableId, 4 SET row, 0 @next ADD row, 1 SET str.DevTableRowId, #<row> SET_LEN n, #<str.DevTableCOL1> COMPARE #<n>, 0 IF=, @done SET tally, KM.#<str.DevTableCOL1> N: #<row>. #<str.DevTableCOL3>: *#<tally> of #<str.DevTableCOL2> so far. GOTO @next @done N: Nothing else is posted. END
The loop needs the pointer from the rules at the top. The kill tally for monster 40 lives in the cookie
KM.40, but the monster number comes from the table, and names do not nest. So the script builds
the name KM.40 in tally and reads through it with *#<tally>. The
same trick works for num.item and num.Trophy and anything else with a number in its
name.
Example: choosing the column at run time
Columns are reached by number too, and the number can come from a cookie: build the column's cookie name and follow it.
; DevTable05.txt -- what each villager says, Sunday to Saturday ; key, then one cell per day of the week bess, "Church day.", "Washing day.", "Baking day.", "Market day.", "Mending day.", "Brewing day.", "Rest day, if I'm lucky." tam, "Hungover.", "Still hungover.", "Sober, mostly.", "Market day. Don't.", "Sober.", "Pay day!", "Ask me tomorrow."
SCENE 200, tavern,, "The Drowned Rat" ACTOR 1, Bess, barmaid, 0, 25, 70 ACTOR 2, Tam, drunk, 0, 75, 70 SET num.DevTableId, 5 SET day, str.DevTableCOL#<num.timeDoW> SET str.DevTableRowId, bess 1: *#<day> SET str.DevTableRowId, tam 2: *#<day> END
num.timeDoW runs from 1 for Sunday to 7 for Saturday, which matches columns 1 to 7 once the key
sits in column 0. The cookie day holds the name str.DevTableCOL3 on a Tuesday, and
*#<day> reads the cell. The row is switched between the two speeches while the column name
stays the same.
Reloading and limits
- Tables are read when the world loads. After editing one, leave the world and enter it again to see the change; a running scene keeps the old values.
- A row may have as many cells as you like, and rows in one table need not have the same number of cells. A column that a row lacks reads as empty.
- The three cookies are shared by every scene in the world. A scene that reads a table should set both
num.DevTableIdandstr.DevTableRowIditself rather than trust what the last scene left. - Reads are plain text. A cell holding a number works anywhere a number does, because the value is pasted
into the line before the command runs:
GIVE G#<str.DevTableCOL2>isGIVE G65by the time it is carried out.