Community Edition “New breath for an old well.”

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.
  • cookieProtection in config.ini adds a hash to stored cookies (1 for names that start with secure, 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

CookieValue
num.hostClassClass number. Action: setting it changes the hero's class (used with HIDDEN_CLASS).
num.hostLevel, num.hostXP, num.hostMaxXP, num.hostTotalXP, num.hostTNLLevel and experience; TNL is the experience still needed for the next level.
num.hostHP, num.hostMaxHP, num.hostMP, num.hostMaxMPCurrent and maximum HP and MP.
num.hostSTR, WIS, STA, AGI, DEX, num.hostAttack, num.hostDefenseAbilities and the attack and defence totals.
num.hostGP, num.hostPP, num.hostTotalPPGold, unspent training points, lifetime training points.
num.hostHandSkill3, num.hostElementSkill3, num.hostHandPP3, num.hostElementPP3Level and points in hand 3 or element 3.
num.hostHandRatio, num.hostMagicRatioThe class's split between weapons and spells.
num.hostX, num.hostYPosition in the scene, 0 to 100.
num.hostAge, num.hostTotalAge, num.hostBirthDay, Month, YearMinutes played by this hero and on this computer, and the hero's birth date.
num.hostEquipId0 ... num.hostEquipId13The item worn in each slot.
num.item12How many of item 12 the hero carries.
num.hostKills, num.hostDeaths, num.hostPKs, num.hostHuntSkill, num.hostHaloPoints, num.hostRatingTallies and ratings.
num.hostPets, num.hostPetLevel, num.hostPetId, num.hostHasPet40Pets owned, the highest pet level, the pet out in the scene, and pets of monster 40 owned.
num.hostBravery, num.hostAggressivenessThe hero's own settings.
str.name, str.uniform, str.soulThe 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

CookieValue
KM.40, KM.0Kills of monster 40, and all kills.
KM.undeadKills of every monster whose alignment word is undead. Counted as the kills happen, so a quest for ten of a kind is one IF.
lastAskThe 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.

CookieValue
item.name, class, price, level, element, attack, defense, hp, mp, ap, desc, fpThe item's columns (ap is the ability points it grants, fp the find chance).
item.goldGiven, item.trophyGivenWhat the last gift to an actor contained.
monster.name, hp, mp, attack, defense, str, sta, agi, dex, wis, level, element, expPts, gold, alignment, skinThe monster's columns.
spell.name, pp, element, damage, level, priceThe 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

CookieValue
num.Trophy7, num.TrophyBagRoom7How many of trophy 7 the hero holds, and how many more would fit.
num.TrophyBagWidth, num.TrophyBagHeightAction: setting them resizes the bag.
num.TrophyBagSlots, num.TrophyBagEmptySlots, num.TrophyBagSlotsInUseSlot counts (action: setting SlotsInUse empties the bag).
num.TrophyBagOpen1 while the bag window is open. Action: 1 shows it, 0 hides it.
num.TrophyIdInSlot3, num.TrophyCountInSlot3What is in slot 3 and how many. Action: setting them fills the slot.
num.TrophyId, then num.TrophyStackSize, StackCost, Probability, Token, FlagsSelect 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

CookieValue
num.timeHour, Minute, Day, Month, Year, DoW, num.timeNowThe player's clock (DoW 1 is Sunday; timeNow is seconds since 1970).
num.serverTimeHour ... num.serverTimeNowThe server's clock, in the player's time zone.
num.countDown, num.timerLeft2, num.timerLength2Seconds 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.wosVersion2817 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.sceneMapFlagsThe 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.closestLinkStyleThe nearest link (-1 when none) and its style.
scene.id, scene.bkgnd, scene.fx, scene.weatherThe current scene.
num.peopleInScene, num.peopleInParty, str.actorName3Head counts and actor names.
num.isPKAttack1 during a player-versus-player fight.
num.isTactics1 in the original's Tactics mode; always 0 in ReBreather.
num.holidayId, str.holidayName, str.holidayGreetingToday's holiday from the player's calendar; the ids, names and greetings are listed below.
event.actor, event.monsterIndex, event.x, event.y, event.missionIdSet by the game when an event label fires.
arg0 ... arg9, numArgsSet 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.

IdHolidayWhen
1Birthdaythe hero's birth month and day
2Eastercomputed for the year
3Thanksgivingthe fourth Thursday of November
4, 5, 6Christmas Eve, Christmas, Boxing Day24, 25 and 26 December
7New Year's Eve, New Year's Day31 December and 1 January (both id 7, both reported as New Year's Eve)
9Groundhog Day8 February
10Valentine's Day14 February
11St. Patrick's Day17 March
12April Fools1 April
13Earth Day22 April
14Cinco de Mayo5 May
15Mothers Daythe second Sunday of May
16Memorial Daythe last Monday of May
17Fathers Daythe third Sunday of June
18Canada Day1 July
19Independence Day4 July
20Labor Daythe first Monday of September
21Columbus Daythe second Monday of October
22Halloween31 October
23Veteran's Day11 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, never DevTable7.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

CookieValue
num.DevTableIdAction: which table the next reads use, 0 to 99. Set it once; it stays until changed.
str.DevTableRowIdAction: the key of the row to read, matched exactly as written in the file, including case.
str.DevTableCOL2Column 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.DevTableId and str.DevTableRowId itself 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> is GIVE G65 by the time it is carried out.