! "Player notepad" extension by David Fisher (version 1.0, November 23rd 2007)
!
! Copyright David Fisher, 2007 ([email protected]).
!
! This extension may be freely used, modified, archived and distributed
! as long as this copyright notice remains intact.
!
! An out-of-game notepad which the player can use to write and edit notes
! during a game.
!
! Note that this extension overrides the BeforeParsing and UnknownVerb
! entry points; if you override these routines in your game, delete the
! versions below and call nbBeforeParsing() or nbUnknownVerb() at the
! end of your routines.
!
! Type "notepad" for usage.
!

[ BeforeParsing;
 nbBeforeParsing();
];

[ UnknownVerb;
 return nbUnknownVerb();
];

[ NotepadInfoSub;
 print "An inbuilt notepad may be used to take notes during the game.
   To add a new note, start a line with
   ~=~. For example:^^
   @@32 =can't find the chicken
   ^^Use ~=~ on a line by itself (or ~notes~) to read your notes so far.
   ^^Each note is assigned a number, starting from 1. You can read an
   individual note by typing its number, or change it by following
   the number with ~=~ and the new text:^^
   @@32 23=there is no chicken
   ^^Lastly, you can erase a note by saying ~erase~ or ~del/delete~
   followed by its number (the rest of the notes are then renumbered).^^
   To read these notes again, type ~notepad~.^";
];

Verb meta 'notepad' 'notebook'
    *           -> NotepadInfo;

Verb meta 'notes'
    *           -> ShowNotes;   ! same thing as "=" on a line by itself

Verb meta 'erase' 'delete' 'del'
    *           -> DeleteNothing
    * number    -> Delete
    * 'notes'/'notepad'/'notebook' -> DeleteAll
    * noun      -> DeleteNoun;

! (used internally; player can't type this verb)
Verb meta '.='
    *           -> Notepad
    * topic     -> Notepad;

! size of notepad (number of characters + 1 byte per note)
Constant NOTE_SIZE 4000;

! a warning is given if there is only this much space left in the notepad
Constant WARN_REMAIN 100;

! the notes are all stored in a single array
Array notes -> NOTE_SIZE + 1;

Global note_end = 0;   ! position of next note (length of notes array)
Global note_num = 0;   ! number of notes

Global notepad_cmd = false;

[ nbUnknownVerb;
 if (notepad_cmd) { return '.='; }
 rfalse;
];

[ nbBeforeParsing    i j;
 notepad_cmd = false;
 j = bufferEndPos();

 ! check for '=' or number

 for (i = WORDSIZE : i <= j : i++)
 {
    if (buffer->i ~= ' ') { break; }
 }

 if (i > j) { return; }

 if (buffer->i == '=' || isDigit(buffer->i))
 {
    ! will be handled by the UnknownVerb entry point
    notepad_cmd = true;

    ! temporarily substitute dots and commas
    for (i++ : i <= j : i++)
    {
       ! will be undone in NotepadSub()
       if (buffer->i == '.') { buffer->i = 1; }
       else if (buffer->i == ',') { buffer->i = 2; }
    }
 }
 Tokenise__(buffer, parse);
];

[ NotepadSub   i j n;
 j = bufferEndPos();
 for (i = WORDSIZE : i <= j : i++)
 {
    if (buffer->i ~= ' ') { break; }
 }

 for (n = i : n <= j : n++)
 {
    ! undo changes done in BeforeParsing()
    if (buffer->n == 1) { buffer->n = '.'; }
    else if (buffer->n == 2) { buffer->n = ','; }
 }

 if (buffer->i == '=')
 {
    for (i++ : i <= j : i++)
    {
       if (buffer->i ~= ' ') { break; }
    }

    if (i > j) { nbList(); }
    else { nbAppend(i); }
 }
 else
 if (isDigit(buffer->i)) { nbNumber(i); }
 else
 {
    print "Sorry, that command was not understood.^";
 }
];

[ ShowNotesSub;
 nbList();
];

[ toUpper ch;

 if (ch >= 'a' && ch <= 'z') { return ch - 'a' + 'A'; }
 return ch;
];

! lists all notes if n_only is 0

[ nbList n_only   n index upper ch start;

 if (note_end == 0)
 {
    print "The notepad is empty.^";
    return;
 }

 index = 1;
 upper = true;
 start = true;

 for (n = 0 : n < note_end : n++)
 {
    if (start)
    {
       if (n_only == 0 or index) { print index, ". "; }
       upper = true;
       start = false;
    }
    if (notes->n == 0)
    {
       if (n_only == 0 or index)
       {
          if (n > 0)
          {
             if (~~(notes->(n-1) == '.' or '!' or '?' ||
                    (n > 1 && notes->(n-1) == '"' &&
                     notes->(n-2) == '.' or '!' or '?')))
             { print "."; }
          }

          new_line;
       }
       index++;
       start = true;
    }
    else
    {
       if (n_only == 0 or index)
       {
          ch = notes->n;

          ! ZCode makes everything lower case; start the sentence with
          ! an upper case letter
          if (upper && ch ~= '.' or '!' or '?' or ' ' or '"' or ''')
          { ch = toUpper(ch); upper = false; }

          print (char) ch;
          if (ch == '!' or '?') { upper = true; }
          else
          if (ch == '.')
          {
             if (n > 0 && notes->(n-1) == '.') { upper = false; }
             else { upper = true; }
          }
       }
    }
 }
];

[ notepadFullMsg;
 print "[The notepad is full. Try deleting unneeded entries.]^";
];

[ nbAppend pos   j len percent;

 j = bufferEndPos();
 while (j >= 0 && j == ' ') { j--; }
 if (j < 0)
 {
    ! should never happen
    print "Sorry, there was a problem understanding the command.^";
    return;
 }

 len = j - pos + 1;

 if (note_end + len + 1 > NOTE_SIZE)
 {
    notepadFullMsg();
    return;
 }

 for ( : pos <= j : pos++)
 {
    notes->note_end = buffer->pos;
    note_end++;
 }

 notes->note_end = 0;
 note_end++;

 note_num++;
 print "Noted.  [", note_num, "]";

 if (NOTE_SIZE - note_end <= WARN_REMAIN)
 {
    ! avoid overflowing 16 bit numbers
    percent = 100 -
       ((NOTE_SIZE - note_end + (NOTE_SIZE / 100) - 1) * 100 / NOTE_SIZE);
    print " (", percent, "% full)";
 }
 new_line;
];

[ isDigit ch;
 return (ch >= '0' && ch <= '9');
];

[ nbNumber pos   i j val;
 j = bufferEndPos();

 for (i = pos : i <= j && isDigit(buffer->i) : i++)
 {
    if (val >= 10000)
    {
       print "Number too large.^";
       return;
    }
    val = val * 10 + (buffer->i - '0');
 }

 ! skip spaces
 for ( : i <= j && buffer->i == ' ' : i++)
 { }

 if (i > j)
 {
    if (checkVal(val)) { nbList(val); }
 }
 else
 if (buffer->i == '=')
 {
    if (checkVal(val,
       "To add a new entry, just say ~=~ followed by the text"))
    { replaceEntry(val, i + 1); }
 }
 else
 {
    print "Extra characters after command (did you mean to say ~", val,
       " = ...~?)^";
 }
];

[ findNBEntry index   n upto;
 upto = 1;
 for (n = 0 : n < note_end && upto < index : n++)
 {
    if (notes->n == 0) { upto++; }
 }

 if (n >= note_end)
 {
    print "[Internal error #1 in notepad.]^";
    return -1;
 }

 return n;
];

! find position of next zero byte in note array
! Returns -1 if goes past end of array

[ findNextZeroPos pos;
 for ( : pos < note_end && notes->pos ~= 0 : pos++)
 { }

 if (pos >= note_end)
 {
    print "[Internal error #2 in notepad.]^";
    return -1;
 }
 return pos;
];

[ bufferEndPos;
 return WORDSIZE + buffer->1 - 1;
];

[ replaceEntry index pos   n n2 i j endpos len old_len;
 j = bufferEndPos();

 ! skip spaces
 for ( : pos <= j && buffer->pos == ' ' : pos++)
 { }

 if (pos > j)
 {
    deleteEntry(index);
    return;
 }

 for (endpos = j : endpos >= pos && buffer->endpos == ' ' : endpos--)
 { }

 if (endpos < pos)
 {
    ! should be impossible, but just in case
    deleteEntry(index);
    return;
 }

 n = findNBEntry(index);
 if (n == -1) { return; }

 n2 = findNextZeroPos(n);
 if (n2 == -1) { return; }

 len = endpos - pos + 1;
 old_len = n2 - n;

 if (note_end + len - old_len > NOTE_SIZE)
 {
    notepadFullMsg();
    return;
 }

 if (len > old_len) { moveNBup(n + old_len, len - old_len); }
 else if (len < old_len) { moveNBdown(n + old_len, old_len - len); }

 for (i = 0 : i < len : i++)
 {
    notes->(n+i) = buffer->(pos+i);
 }

 print "Replaced.^";
];

[ moveNBup from_pos amount   n;
 for (n = note_end - 1 : n >= from_pos : n--)
 { notes->(n+amount) = notes->n; }

 note_end = note_end + amount;
];

[ moveNBdown from_pos amount   n;
 for (n = from_pos : n < note_end : n++)
 { notes->(n-amount) = notes->n; }

 note_end = note_end - amount;
];

[ checkVal val empty_msg_str;
 if (val <= 0)
 {
    print "The notepad is numbered from 1.^";
    rfalse;
 }

 if (note_num == 0)
 {
    print "The notepad is empty.^";
    if (empty_msg_str ~= nothing)
    {
       new_line;
       print (string) empty_msg_str;
       print ".^";
    }
    rfalse;
 }

 if (val > note_num)
 {
    print "There ";
    if (note_num == 1) { print "is only one entry"; }
    else { print "are only ", (number) note_num, " entries"; }
    print " in the notepad.^";
    rfalse;
 }
 rtrue;
];

[ DeleteNothingSub;
 if (note_num == 0)
 {
    print "The notepad is empty.^";
 }
 else
 if (note_num == 1)
 {
    print "(notepad entry 1)^^";
    deleteEntry(1);
 }
 else
 {
    print "You'll have to say which notepad entry to delete (1-",
       note_num, ").^";
 }
];

[ DeleteAllSub;
 print "You can only erase one entry from the notepad at a time.^";
];

[ DeleteNounSub;
 print "You can't delete ", (the) noun, "!^";
];

[ DeleteSub    index;
 index = noun;
 if (~~checkVal(index)) { return; }
 deleteEntry(index);
];

[ deleteEntry index   n n2;
 n = findNBEntry(index);
 if (n == -1) { return; }

 n2 = findNextZeroPos(n);
 if (n2 == -1) { return; }

 moveNBdown(n2 + 1, n2 - n + 1);
 print "Deleted.^";
 note_num--;
];

Object the_notepad "notepad"
has scenery
with name 'notepad' 'notebook' 'notes',
found_in [; rtrue; ],
before [;
  meta = true;
  ! (don't assume that Examine = Read or that there is a ##Read action)
  if (action == ##Examine || verb_word == 'read')
  {
     ShowNotesSub();
     if (self.gave_tip == false)
     {
        self.gave_tip = true;
        print "^[Tip: you can just say ~=~ or ~notes~ to read your notes.]^";
     }
     rtrue;
  }
  else
  {
     print "The notepad is not a physical object in the game, it is
        just a way to write notes as you play. Type ~notepad~ for more
        information.^";
  }
  rtrue;
],
gave_tip false;