Jump to content

Home

What SCUMM actually looked like


Recommended Posts

Posted (edited)

Thought someone might find this interesting.

 

The broader public finally got a proper view of actual SCUMM in the Video Game History Foundation's brilliant interview with Ron a couple of years ago. But for a bit of a wider glance, here's an example of what the script for a full room would look like.

 

This is output from a project I started on a couple of years ago, which is currently in hiatus. Based on a lot of research and archaeology.

 

First and foremost - this is not an original script (as should be somewhat evident, if you read it). I've never seen an original script from Maniac Mansion. This is reconstructed from the game files. It is, however, 100% actual SCUMM syntax. Possibly with some minor unknown differences compared to an actual 1987 SCUMM script - the syntax is accurate to SCUMM circa 1990-1991 - that is, the SCUMM compiler for MI1 or MI2 would accept it as proper SCUMM. The actual syntax never changed much between Maniac Mansion on C64 to CMI. And the commands, other than additions, rarely changed either, at least from Maniac Mansion for PC and on.

 

Other than that, the script is from Maniac Mansion (obviously) - the PC version. Although my "decompiler" actually handles all games from MM C64 to CMI, there were some early hacks in C64 that it doesn't quite like yet, and which makes the MM C64 output slightly less readable - most notably that objects and actors shared references, meaning it has a harder time naming objects and actors where they overlap.

 

Stuff worth noting:

  • A few variables, objects, scripts, and defines haven't been named by me. Yes, those names are all mine, but based on how variables etc. would typically be named in actual game scripts. I've only done these "annotations" for Maniac Mansion so far.
  • Even the way indentation was done is reconstructed by the tool - 3 spaces were used, but lines of dialog were mostly always indented to align perfectly.
  • Obviously one of the most interesting things - comments - cannot be reconstructed from game files. 😉
  • The decompiler doesn't yet handle cases where names have aliases. For example, "OFF", "CLOSED", "HERE" (yes) and "R-GONE" are all aliases for the same thing - SCUMMlets used whatever was appropriate in the context (or they'd be chastised by Ron or Tim 😂). Here, I've manually renamed most of those. And a few additional line breaks. There's no AI involved 😉, so the decompiler doesn't actually know where code could appropriately be split into sections.
  • Some of the commands may actually be "expanded" code that was originally macros.
  • Yes, the cases of "jump" commands jumping into different blocks were also actual common SCUMM practice.

 

Other interesting things about this particular script:

  • Around halfways down, the definition of the room starts. This is the standard (and required) layout in SCUMM - room scripts followed by the room definition - sounds, enter/exit scripts, and object definitions including their verb scripts.
  • Since this is the PC version, while the play-cricket-sound script is still there, it doesn't actually do anything.
  • The "userface" command is one of the few that I have no idea how actually looked.
  • "wait-for-message" is one of the additions compared to the C64 version - where the equivalent SCUMM code would be "break-until (message-going is false)" - although it might already have been turned into a "wait-for-message" macro in the original game. Starting with the MM PC version, it turned into a native command.

 

Enough talk...

 

script play-cricket-sound {
   do {
      sleep-for 100 minutes
   }
}

script kill-a-kid {
   break-here
   cut-scene {
      if (cause-of-death is radioactive-steam) {
         say-line "Oh no! Radioactive steam! Ahheeeeeeee^"
         do {
            do-animation selected-actor turn-left
            break-here
            do-animation selected-actor turn-front
            break-here
            do-animation selected-actor turn-right
            break-here
            do-animation selected-actor turn-back
            break-here
            x = random 70
            y = random 12
            y += 56
            walk selected-actor to x,y
         } until (message-going is false)
         who-dies is selected-actor
      }
      
      if (cause-of-death is drowning-in-pool) {
         current-room pool
         camera-at 41
         sleep-for 2 seconds
         print-line "Glug! Glug! Glug! Glug!"
         wait-for-message
         sleep-for 3 seconds
      }
      
      if (cause-of-death is ed-seeing-hamster) {
         stop-script weird-ed-chases-kid
         wait-for-message
         say-line weird-ed "Wait! What IS that?"
         wait-for-message
         say-line weird-ed "It has bits of fur like my hamster's!"
         wait-for-message
         say-line weird-ed "Oh no!!! What did you do!!! Argh!!!"
         wait-for-message
         who-dies is selected-actor
         put-actor weird-ed at 10,52 in-room weird-eds-room
      }
      
      if (cause-of-death is flesh-eating-plant) {
         who-dies is selected-actor
         current-room plant-room
         camera-at 28
         sleep-for 2 seconds
         print-line "YUM!!"
         sleep-for 3 seconds
      }
      
      if (cause-of-death is death-by-tentacle) {
         wait-for-message
         who-dies is selected-actor
      }
      
      actor who-dies costume costume-0
      kid-is-busy[who-dies] = 1
      foo = 253
      
      do {
         if (owner-of foo is who-dies) {
            owner-of foo is nuked
         }
         foo -= 1
      } until (foo == 0)
      
      ++number-of-dead-kids
      
      if (who-dies is dave) {
         dave-is-dead is true
      }
      
      if (number-of-dead-kids == 1) {
         state-of tombstone-1 is HERE
         class-of tombstone-1 is TOUCHABLE
         put-actor who-dies at 106,64 in-room mansion-exterior
      }
      
      if (number-of-dead-kids == 2) {
         state-of tombstone-2 is HERE
         class-of tombstone-2 is TOUCHABLE
         put-actor who-dies at 114,64 in-room mansion-exterior
      }
      
      if (number-of-dead-kids == 3) {
         state-of tombstone-3 is HERE
         class-of tombstone-3 is TOUCHABLE
         put-actor who-dies at 122,64 in-room mansion-exterior
      }
      
      foo = actor-x who-dies
      current-room mansion-exterior
      camera-at foo
      sleep-for 6 seconds
   }
   
   if (number-of-dead-kids == 3) {
      start-script game-over
   }
   
   userface 1 unfreeze-scripts cursor-on sentence inventory verbs
}

script open-film {
   if (develop-film-step is film-opened) {
      say-line "It's already opened."
   }
   
   if (develop-film-step is film-closed) {
      say-line "Ok, it's opened."
      wait-for-message
      
      do {
         foo is owner-of film
         if (foo == 0) {
            stop-script
         }
         
         bar is actor-room foo
         
         if (room-is-dark[bar] == 0) {
            if (state-of circuit-breakers is OFF) {
               if (selected-room is bar) {
                  say-line "I think I just exposed the film."
               }
               develop-film-step is film-opened
            }
         }
         
         sleep-for 15 seconds
      } until (develop-film-step is-not film-closed)
   }
}

script deliver-envelope {
   sleep-for 1 minute
   
   do {
      sleep-for 30 seconds
   } until (selected-room is-not mansion-exterior)
   
   if (state-of envelope is OFF) {
      stop-script
   }
   
   state-of envelope is OFF
   class-of envelope is UNTOUCHABLE
   state-of mailbox-flag is OFF
   owner-of sealed-envelope-in-safe is nuked
   sleep-for 3 minutes
   cut-scene {
      current-room room-0
      start-script marketeer-reacts-to-envelope-contents
      break-until (script-running marketeer-reacts-to-envelope-contents is false)
   }
   sleep-for 5 minutes
   if (contract-status > unsigned) {
      do {
         sleep-for 30 seconds
      } until (selected-room is-not mansion-exterior)
      
      state-of contract is R-HERE
      class-of contract is TOUCHABLE
      start-sound doorbell-sound
   }
}

script schedule-package-delivery {
   sleep-for 5 minutes
   do {
      sleep-for 5 seconds
      if (selected-room is-not mansion-exterior) {
         if (selected-room is-not weird-eds-room) {
            if (actor-room weird-ed is weird-eds-room) {
               jump deliver
            }
         }
      }
   }
   
deliver:
   start-sound doorbell-sound
   state-of package is HERE
   class-of package is TOUCHABLE
   do {
      sleep-for 8 seconds
   } until (actor-room weird-ed is weird-eds-room)
   
   if (selected-room is-not mansion-exterior) {
      start-sound doorbell-sound
   }
   
   stop-script touch-stuff-in-weird-eds-room
   weird-ed-downstairs-reason is doorbell
   start-script send-weird-ed-downstairs
}

script read-contract {
   case contract-status {
      of signed-kid {
         case who-recorded-demo {
            of selected-actor {
               say-line "Wow! It's a recording contract for ME!"
            }
            
            of syd {
               say-line "It's a recording contract for Syd."
            }
            
            default {
               say-line "It's a recording contract for Razor."
            }
         }
      }
      
      of signed-green {
         say-line "It's a recording contract",
                  "for Green Tentacle."
      }
      
      default {
         say-line "It's a book publishing contract,",
                  "and it's worth MILLIONS!"
      }
   }
}

script meanwhile-in-lab-1 {
   sleep-for 1 minute
   
   cut-scene {
      override skip-meanwhile-1
      
      state-of lab-door-right is OPEN
      put-actor dr-fred at 15,58 in-room lab
      put-actor sandy at 25,58 in-room lab
      do-animation dr-fred turn-right
      do-animation sandy turn-left
      
      current-room lab
      camera-at 20
      sleep-for 1 second
      
      say-line dr-fred "Well, my dear. Hope you're having fun!":
                       "Within minutes it'll all be over.":
                       "You'll be hooked up to my machine",
                       "getting your pretty brains sucked out."
      walk dr-fred to sandy within 5
      break-until (chars-printed > 66)
      
      do-animation sandy turn-front
      do-animation sandy chore-12
      wait-for-message
      say-line sandy "You'll never get away with this!":
                     "Dave and his friends will rescue me!":
                     "You and your meteor can eat slime!"
      break-until (chars-printed > 45)
      
      do-animation sandy turn-left
      wait-for-message
      foo = actor-x dr-fred
      walk dr-fred to foo,68
      sleep-for 1 second
      say-line dr-fred "That's what she thinks!"
      wait-for-message
      
      walk dr-fred to lab-door-right
      say-line dr-fred "Heh, heh, heh."
      wait-for-actor dr-fred
      put-actor dr-fred in-the-void
      
      state-of lab-door-right is CLOSED
      start-sound close-door-sound
      
      walk sandy to lab-door-left
      wait-for-actor sandy
      do-animation sandy turn-back
      say-line sandy "Help, help, HELP!"
      wait-for-message
      sleep-for 1 second

skip-meanwhile-1:
      print-line " "
      state-of lab-door-right is OFF
   }
   put-actor sandy in-the-void
   start-script schedule-meanwhile-in-lab-2
}

script meanwhile-in-lab-2 {
   cut-scene {
      override skip-meanwhile-2
      
      put-actor dr-fred at 50,62 in-room lab
      put-actor sandy at 30,62 in-room lab
      put-actor purple-tentacle at 40,62 in-room lab
      do-animation sandy turn-front
      
      current-room lab
      camera-at 20
      sleep-for 1 second
      
      walk purple-tentacle to sandy within 3
      wait-for-actor purple-tentacle
      do-animation sandy turn-left
      say-line sandy "Get away from me you purple slime geek."
      walk sandy to 5,50
      break-here 2
      
      walk purple-tentacle to 5,50
      wait-for-actor sandy
      walk sandy to 25,45
      say-line sandy "Don't touch me!"
      wait-for-actor purple-tentacle
      walk purple-tentacle to 21,50
      wait-for-actor sandy
      
      walk dr-fred to 30,50
      wait-for-actor dr-fred
      do-animation dr-fred turn-left
      say-line dr-fred "PURPLE TENTACLE!!":
                       "Stop playing with the lab experiments.":
                       "Bring her, the machine is ready.":
                       "Heh, heh, heh."
      break-until (chars-printed > 90)
      
      walk dr-fred to 60,50
      wait-for-message
      do-animation sandy turn-front
      say-line sandy "EEEEEEEEEK!!!!"
      do-animation sandy chore-12
      wait-for-message
      sleep-for 1 second

skip-meanwhile-2:
      print-line " "
   }
   put-actor sandy in-the-void
}

script schedule-meanwhile-in-lab-2 {
   sleep-for 45 minutes
   start-script meanwhile-in-lab-2
}

room mansion-exterior {
   sounds {
      "sfx\unused-sound-5" unused-sound-5
      "sfx\doorbell-sound" doorbell-sound
   }

   enter {
      lights is 2
      stop-sound radiation-sound
      
      if (script-running win-game is false) {
         start-script play-cricket-sound
      }
   }

   exit {
      stop-script play-cricket-sound
      stop-sound cricket-sound
      
      if (state-of envelope is R-HERE) {
         if (envelope-stamped is true) {
            if (envelope-is-addressed is true) {
               if (state-of mailbox is CLOSED) {
                  if (state-of mailbox-flag is R-HERE) {
                     if (script-running deliver-envelope is false) {
                        start-script deliver-envelope
                     }
                  }
               }
            }
         }
      }
      
      if (script-running script-12 is true) {
         start-sound radiation-sound
      }
   }

   object front-door-ext {
      name is "front door"
      class is LOCKED
      
      verb open {
         if (class-of current-noun1 is UNLOCKED) {
            start-script open-door
            state-of front-door-int-left is OPEN
         } else {
            start-script door-locked-response
         }
      }

      verb close {
         start-script close-door
         state-of front-door-int-left is CLOSED
      }

      verb unlock use {
         if (current-noun2 is key) {
            class-of current-noun1 is UNLOCKED
            do-sentence open front-door-ext
         } else {
            start-script cant-unlock-response
         }
      }

      verb lock {
         class-of current-noun1 is LOCKED
         do-sentence close front-door-ext
      }

      verb walk-to {
         if (state-of current-noun1 is OPEN) {
            if (entered-house is false) {
               entered-house is true
               start-script meanwhile-in-lab-1
               
               put-actor nurse-edna at 62,56 in-room kitchen
               do-animation nurse-edna turn-back
               
               state-of refrigerator is OPEN
               start-script script-152
            }
            come-out-door front-door-int-left in-room hall
         }
      }

   }

   object door-mat {
      name is "door mat"
      
      verb pull pick-up {
         if (state-of current-noun1 is GONE) {
            say-line "I'll leave it here."
         } else {
            state-of current-noun1 is GONE
         }
      }

      verb push {
         state-of current-noun1 is HERE
      }

   }

   object key {
      name is "key"
      class is PICKUPABLE
      
      dependent-on door-mat being GONE
      
      verb pick-up {
         pick-up-object current-noun1
      }

      verb use {
         if (current-noun2 is front-door-ext) {
            class-of front-door-ext is UNLOCKED
            do-sentence open front-door-ext
         } else {
            say-line "It doesn't work."
         }
      }

   }

   object mailbox {
      name is "mailbox"
      
      verb open {
         state-of current-noun1 is OPEN
      }

      verb close {
         state-of current-noun1 is CLOSED
      }

      verb use {
         do-sentence use current-noun2 mailbox-open
      }

   }

   object mailbox-open {
      name is "mailbox"
      
      verb open {
         state-of mailbox is OPEN
      }

      verb close {
         state-of mailbox is CLOSED
      }

      verb use {
         if (current-noun2 is sealed-envelope-in-safe) {
            if (state-of mailbox is OPEN) {
               if (envelope-is-addressed is false) {
                  say-line "There is no address on it."
               } else {
                  if (envelope-stamped is false) {
                     say-line "There's no stamp on it."
                  } else {
                     if (content-in-envelope == envelope-empty) {
                        say-line "The envelope is empty."
                     } else {
                        owner-of sealed-envelope-in-safe is nobody
                        class-of envelope is TOUCHABLE
                        state-of envelope is R-HERE
                     }
                  }
               }
            }
         }
      }

      verb read {
         say-line "Solicitors will be eaten."
      }

   }

   object package {
      name is "package"
      state is GONE
      class is PICKUPABLE UNTOUCHABLE

      verb pick-up {
         pick-up-object current-noun1
         if (got-stamps is false) {
            pick-up-object stamps
            owner-of stamps is nobody
         }
      }

      verb give {
         if (current-noun2 < end-kids) {
            owner-of current-noun1 is current-noun2
         }
         if (current-noun2 is weird-ed) {
            start-script give-package-to-weird-ed
         }
      }

      verb read {
         say-line "To: Weird Ed"
      }

      verb open pull {
         if (got-stamps is false) {
            if (state-of current-noun1 is GONE) {
               owner-of stamps is selected-actor
            } else {
               pick-up-object stamps
            }
            say-line "Some uncanceled stamps came off!"
            got-stamps is true
         } else {
            say-line "That would be illegal."
         }
      }

   }

   object stamps {
      name is "stamps"
      class is PICKUPABLE
      
      dependent-on package being OFF
      
      verb pull pick-up {
         pick-up-object current-noun1
         say-line "Hmm, they're uncanceled."
         got-stamps is true
      }

      verb use {
         if (current-noun2 is envelope-in-microwave or current-noun2 is sealed-envelope-in-safe) {
            if (envelope-unsealed == 1) {
               envelope-stamped is true
               say-line "They stick!"
               start-script update-envelope-name
               owner-of current-noun1 is nuked
            } else {
               jump wont-stick
            }
         } else {
wont-stick:
            say-line "They won't stick."
         }
      }

   }

   object mailbox-flag {
      name is "flag"
      
      verb open {
         do-sentence open current-noun1 current-noun2
      }

      verb close {
         do-sentence close current-noun1 current-noun2
      }

      verb pull {
         state-of current-noun1 is GONE
      }

      verb push {
         state-of current-noun1 is HERE
      }

      verb use {
         if (state-of current-noun1 is GONE) {
            state-of current-noun1 is HERE
         } else {
            state-of current-noun1 is GONE
         }
      }

   }

   object bushes-left {
      name is "bushes"
      verb open pull pick-up {
         state-of current-noun1 is GONE
      }

   }

   object grating {
      name is "grating"
      class is LOCKED
      
      dependent-on bushes-left being GONE
      
      verb open {
         if (class-of current-noun1 is LOCKED) {
            if (used-hunk-o-matic[selected-actor] == 1) {
               say-line "Easy!"
               jump open-grating
            } else {
               say-line "I can't budge it. It's rusted shut."
            }
         } else {
open-grating:
            state-of current-noun1 is OPEN
            state-of crawl-space-grate is OPEN
            start-sound open-door-sound
         }
      }

      verb close {
         state-of current-noun1 is CLOSED
         state-of crawl-space-grate is CLOSED
      }

      verb push pull {
         if (state-of current-noun1 is CLOSED) {
            do-sentence open grating
         } else {
            do-sentence close grating
         }
      }

      verb fix unlock use {
         if (current-noun2 is tools) {
            class-of current-noun1 is UNLOCKED
            do-sentence open grating
         }
      }

      verb walk-to {
         if (state-of current-noun1 is OPEN) {
            come-out-door crawl-space-grate in-room crawl-space
         }
      }

   }

   object film {
      name is "undeveloped film"
      state is R-HERE
      class is PICKUPABLE UNTOUCHABLE
      
      verb pick-up {
         pick-up-object current-noun1
      }

      verb open {
         if (script-running open-film is false) {
            start-script open-film
         }
      }

      verb use {
         do-sentence use current-noun2 current-noun1
      }

      verb read {
         if (develop-film-step < film-developed) {
            say-line "Kodak."
         } else {
            say-line "Looks like photographs of Ed's plans."
         }
      }

      verb give {
         if (current-noun2 < end-kids) {
            owner-of current-noun1 is current-noun2
         }
         if (current-noun2 is weird-ed) {
            if (develop-film-step < film-developed) {
               say-line weird-ed "No! No! You have to develop it for me!"
            } else {
               start-script give-film-to-weird-ed
            }
         }
      }

   }

   object envelope {
      name is "envelope"
      class is UNTOUCHABLE
      
      dependent-on mailbox being OPEN
      
      verb read {
         if (envelope-is-addressed is false) {
            say-line "It's a blank envelope."
         } else {
            say-line "It's addressed to: 222 Skyscraper Way."
         }
      }

      verb pick-up {
         class-of envelope is UNTOUCHABLE
         state-of envelope is R-GONE
         owner-of sealed-envelope-in-safe is selected-actor
      }

   }

   object contract {
      name is "contract"
      class is PICKUPABLE UNTOUCHABLE
      
      dependent-on mailbox being OPEN
      
      verb pick-up {
         pick-up-object current-noun1
         state-of current-noun1 is R-GONE
      }

      verb read {
         start-script read-contract
      }

      verb give {
         start-script give-contract
      }

   }

   object tombstone-1 {
      name is "tombstone"
      state is GONE
      class is UNTOUCHABLE
      
      verb read {
         say-line "And good riddance!"
      }

   }

   object tombstone-2 {
      name is "tombstone"
      state is GONE
      class is UNTOUCHABLE
      
      verb read {
         say-line "Another one bites the dust!"
      }

   }

   object tombstone-3 {
      name is "tombstone"
      state is GONE
      class is UNTOUCHABLE
   }

   object exit-mansion-ext-right {
      name is ""
      verb walk-to {
         come-out-door exit-mansion-gate-left in-room mansion-gate
      }

   }

   object bushes-right {
      name is "bushes"
   }

   object doorbell {
      name is "doorbell"
      
      verb push use {
         start-sound doorbell-sound
         
         if (var-92 == 0) {
            if (script-running send-weird-ed-downstairs is false) {
               if (actor-room weird-ed is weird-eds-room) {
                  weird-ed-downstairs-reason is doorbell
                  start-script send-weird-ed-downstairs
               }
            } else {
               start-script impatient-doorbell-ringing
            }
         }
      }

      verb read {
         say-line "This is the home of Dr. Fred,":
                  "Nurse Edna, Weird Ed, Dead Cousin Ted,":
                  "Green Tentacle and Purple Tentacle."
      }
   }
}
Edited by Serge
Fixed a bad manual rename of contract R-HERE vs R-GONE. Experiment with C style syntax highlighting.
  • Thanks 4
  • Chef's Kiss 2
Link to comment
Share on other sites

Posted (edited)
2 hours ago, AndywinXp said:

This is so great! Love it, thanks for the insights; please post more if you have it 😁

 

Just testing the waters, for now. The full MM decompile might show up at some point. Not sure when I'll have time to annotate the larger games with actor names, object names, room names etc. - or add the missing features that mostly have to do with "intelligently" figuring out what aliases to use - some aren't straightforward to figure out programmatically - like indirection for state-of, where it actually needs to follow the assignments in order to figure out if the object in question might be a door or a switch or another kind of object - and then pick e.g. "CLOSED" or "OFF" or "HERE" (or R-GONE) depending on context. Also want to add an easy way to define "reverse macros" - so that command sequences that are known to be a result of macro expansion are turned back into macro calls.

 

Oh, the thing about HERE and R-GONE (and GONE and R-HERE). The objects' states also control whether (and later what) object images are displayed. Hence why "OFF" = "HERE": The convention in SCUMM was to draw objects onto the background, with the object image actually being an image of the object removed - i.e. the actual background behind the object. Hence, the default, "off", state (0) would be "HERE", and the "on" state would be "GONE". When artists didn't follow that convention, you used R-HERE and R-GONE - with R standing for "Reversed" - and e.g. R-GONE being = HERE. In order to have script readability without changing the art.

 

But just to demonstrate how the syntax and commands changed relatively little until CMI (even though DOTT introduced a completely rewritten compiler and interpreter) - here's a bit from CMI. Without annotations naming all the variables, actors, costumes, objects etc., it becomes a fair bit less easy to read, though. It also shows a good example of macro expansion - the bits with "Too many ambient sounds" warnings messages were clearly originally a much shorter macro call, expanded on compilation:

 

https://gist.github.com/Jither/fa3d05100e4106a125abffb3a6760249

 

ETA: The same thing goes for kludge commands inthere - although the decompiler could already give them their proper names, at the time the scripts were written, they were "commands in training" and were actually named by wrapping them inside a macro (so that's what it's intended that the decompiler eventually does too - use the "reverse macro expansion" to name them). iMUSE also had its own set of "sound kludge" commands wrapped in macros. It was never promoted to first class citizen in SCUMM. Heck, its macros even lived in a file named SOUNDCRP.SCU... 😁

 

ETA 2: Other syntax-related tidbits that I didn't mention above (but there are so many that could be mentioned):

 

  • start-script was mostly optional - you could just write play-cricket-sound rather than start-script play-cricket-sound, although the longer form was also allowed.
  • is, are and == (and =) were all synonyms - as were is-not and != - the decompiler tries to choose the output form much like SCUMMlets would - if it's comparing to or assigning a number, use =, otherwise, is. The "intelligent alias" stuff would also eventually allow e.g. object names etc. to be annotated as plural, so that it can choose are when appropriate.
  • a bit more technical: in case anyone with some parser knowledge is wondering about the infamously unconventional (for modern languages) use of the hyphen as both a valid identifier character and subtraction operator, the way it worked was simply that any hyphen between other identifier characters would be parsed as an identifier - spaces were required to make e.g. a - b a subtraction rather than an identifier (a-b). For the same reason, SCUMM only had prefix syntax for decrement (and increment) - --x - rather than x--, which would be parsed as an identifier. The hyphen wasn't valid as the first character of an identifier (e.g. -selected-number) - in that case, it would be parsed as a subtraction or negation.

 

ETA3: Oh yes, I forgot (it's more than a year since I last actually worked on the decompiler) - automatic naming of rooms, objects etc. based on the info in the game files was off - the CMI example is a bit more readable now.

Edited by Serge
  • Like 3
  • Thanks 1
Link to comment
Share on other sites

Just a bit of "behind the scenes". This is how the annotations for the decompiler look. The syntax is largely based on the syntax of SCUMM's actual "define" files, which are mostly just lists of names with a number assigned. "Largely based" - other than the braces (and annotations after the numeric value), which are added because the decompiler needs a bit more context than a compiler does: To the compiler, a name maps to a single numeric value. But to a decompiler, a numeric value can map to several different names - rooms, actors, objects, variables, states, classes, named variable values... and e.g. an actor number can map to several different actor names. For example, Kate and Governor Phatt have the same actor number in MI2. This would eventually be solved, by e.g. having an annotation like "governor-phatt = 5 when room is phatt-mansion"

 

https://gist.github.com/Jither/c9b416aea9c5ac8ee9684500d5005289

Link to comment
Share on other sites

Speaking of comments and variable names, I seem to remember BGBennyBoy talking about a demo that wasn't compiled properly and left all the debugging profiles active. I can't recall which one, but I wonder if it also included a complete unexpurgated SCUMM script?

Link to comment
Share on other sites

Posted (edited)
2 hours ago, ThunderPeel2001 said:

Speaking of comments and variable names, I seem to remember BGBennyBoy talking about a demo that wasn't compiled properly and left all the debugging profiles active. I can't recall which one, but I wonder if it also included a complete unexpurgated SCUMM script?

 

Unfortunately not 🙂 - as far as I recall, there are two publicly available SPUTM executables that were compiled in debug mode and with Windex support (windex being SCUMM/SPUTM's debug mode which allows debugging the game on a second, monochrome, monitor): The MI2 non-interactive demo, and the MI2 talkie prototype. Both include a lot of extra info in the EXE - function names etc. - and the Windex debuggers obviously bring a lot of insight into internal terminologies and jargon - the talkie will even output a disassembly of the current script using the original command names, with somewhat accurate command syntax, but obviously more in assembly form than in actual script form. BG has a walkthrough and screenshot of Windex in this excellent article:

 

https://mixnmojo.com/features/sitefeatures/LucasArts-First-Words/2

 

ETA: "Somewhat accurate command syntax": The Windex debugger just shows the current byte code formatted into strings that - for the convenience of the SCUMMlet - reads somewhat like the original lines. Without a symbol file, much like the decompiler, it doesn't display variable names, actor names, label names, etc. And by the time a SCUMM game had been compiled:

 

  • loops and conditionals have been turned into jumps ("goto"s)
  • cutscene blocks have been turned into start/end commands
  • break-until has been turned into a loop - and, in turn, into conditional jumps.
  • some commands have been "unrolled" - for example, break-here 17 would turn into simply 17 break-here's in a row
  • if ([some arithmetic]) would have the arithmetic moved out before the if statement: If statements in the final byte code only check a single value in a variable or - from DOTT and on - a value on the stack that is calculated before the if. Those calculations are themselves stack-based, so you wouldn't see a nice if (actor-x guybrush >= actor-x elaine - 15).
  • Some commands had multiple nice ways of being called, where parts could be left out (similar to overloaded functions in several languages) - the compiler - and Windex - would turn those into a single command with various constants indicating "default" added.
  • Windex had no knowledge of macros, so e.g. all iMUSE commands would be listed as sound-kludge with a long list obscure numeric parameters.

 

That's some of the stuff the decompiler "undoes".

Edited by Serge
  • Like 1
  • Thanks 1
  • Chef's Kiss 1
Link to comment
Share on other sites

Posted (edited)

Question: Why would that particular MM script include

script kill-a-kid

?

 

I assume that was one of the macro functions you mentioned, but why would be be included in the script that appears to be associated with the location outside of the mansion? Was it included in every script... or was it something that was being setup because it was the first location in the game? Just seems strange to me.

Edited by ThunderPeel2001
Link to comment
Share on other sites

Posted (edited)
1 hour ago, ThunderPeel2001 said:

Question: Why would that particular MM script include

script kill-a-kid

?

 

I assume that was one of the macro functions you mentioned, but why would be be included in the script that appears to be associated with the location outside of the mansion? Was it included in every script... or was it something that was being setup because it was the first location in the game? Just seems strange to me.

 

Nah, macros are gone (inlined into the script code) when the scripts are compiled - and currently, the decompiler doesn't reconstruct any macros, just leaves the inlined code as is.

 

All the scripts in these very early games are global scripts (ETA: Outside the room definition - the verb scripts aren't global). Later, SCUMM would add local scripts which are automatically stopped when you exit a room. Any room can call a global script (you'll also see the example calling lots of scripts like give-package-to-weird-ed which aren't defined in this room). The guideline was (at least later) to define them in the room where they're most likely to be called (or which has also defined resources the script uses). Of course, you wouldn't want to place a single script in a room stored on disk 2 that's called from a room on disk 1. In this case, the room is "extra global" to the game - it's called from lots of places, depending on where a kid is when they die. But it does have the shared outcome of ending up outside the mansion, at the tombstones.

 

This is a general principle in SCUMM, all the way up to CMI: There are global resources that are needed in many rooms, but they still "belong to" - and are stored - in (mostly) just one of them: The room itself, global scripts, costumes, sounds, character sets (in later games) - and to some extent objects. Although an object could only actually be visible in the room it belonged to - that's also why (when/if I get to releasing other scripts) you'd see that the hamster in Ted's cage is not actually the same hamster as in the microwave (so he really has no reason for getting so angry 😂).

 

In the end, like almost everything else, the compiler just turns these globals into a number in the scripts that reference it. And then it stores the disk and room number for each resource - and their offset within the room - in an index on each disk. That's the "00.lfl" file, or, in later games e.g. "tentacle.000". When a script somewhere needs one of these resources, it's just looks up in the index and loads the resource from the room where it's stored. The rest of the room isn't loaded, so it doesn't add any overhead, and it could theoretically have been stored anywhere on the disk. Assigning it to a room just gives you a bit of control over where on disk it's stored - a bit of micro management to reduce seeking during disk access.

Edited by Serge
  • Like 1
  • Thanks 2
Link to comment
Share on other sites

Posted (edited)
8 minutes ago, ThunderPeel2001 said:

Ahhh... Of course. I think I knew that, that's why there's 00.lfl files on different disks.

 

 

Yeah, you can also see the principle in e.g. SCUMMRev:

 

image.png

Here, each global script ends up in "SCRP". The costumes and sounds that the room script declares are in SOUN and COST. The local scripts - which are only loaded when the room is - are in the LSCR chunks, and "entry" and "exit" scripts in EXCD and ENCD. Object definitions end up in OBCD along with their verb scripts.

 

But note that SCRP, SOUN and COST are actually "outside" the ROOM (but inside LFLF) - they're not "really" part of the room, but associated with it.

Edited by Serge
  • Thanks 1
Link to comment
Share on other sites

9 minutes ago, ThunderPeel2001 said:

Brilliant. I've always wanted to know more about this. It's an interesting (and I guess quite antiquated) way of organising things -- but it also makes perfect sense for the storage methods SCUMM was designed for.

 

Well, it's not all that antiquated - it still makes sense. 🙂 We're just dealing with larger files where some of the same principles apply - because they're large or because they need to accommodate being transferred over networks etc. So, for a video file, it might make sense on a superficial level to store the video in one block, then the audio, then subtitles, etc. But of course, all video containers - AVI, QuickTime/MP4, MKV etc. - interleave these things by some number of frames, so that the player doesn't have to read all over the files to put a frame and its sound together. Similarly, a lot of file formats might e.g. store some header information at the start and other information at the end (this goes for e.g. zip, although to be fair, that's also an "antiquated" format) - mostly because a lot of information about the file isn't actually known until you're done writing it - but you still want as much information as possible at the start, so that you can read parts of the file before you reach the end (if it's being transferred over a network, you might not even have the end when you need to start reading). OpenEXR, used for images mainly in the film industry (but also more and more outside) also has quite a few quirks that are made to avoid seeking in the file.

 

Some parts of SCUMM show their age more - like garbage from old builds, because SCUMM by default only wrote the parts that changed since last build - meaning assets and offsets that were no longer in use might still be left in there. For example, there are a few indexes to assets from MI1 EGA in the VGA build, simply because the VGA build was branched out from the EGA build - those offsets just point into a random offset in the room, but since they aren't ever referenced in the game, it doesn't matter to the game, but does confuse tools that try to read all assets from the files. 🙂 There are also bits of garbage leftover script at the end of some scripts, because offsets weren't changed unless needed - since that would require rewriting everything in the room following that script. Again, wouldn't matter to the game, because it would reach a "end of script" bytecode before reaching the garbage. But still, that also happens intentionally in some modern files - e.g. to avoid rewriting an entire video file just because title or whatever in the header got shorter. 🙂

Link to comment
Share on other sites

Posted (edited)

A few more notes on the CMI example:

  • The scripts that have numbers from 2000 and up are actually local scripts. Whichi is why they're defined inside the room definition (defining a script inside the room definition is how you would signify that a script was local - again, MM didn't have local scripts). There aren't any global scripts in the "treasure" room - hence, the room definition starts at the very top of the file.
  • The strings wouldn't actually start with (e.g.) "/TRNZ365/" while writing. That would likely be added by the compiler or another tool before compiling.
  • We all know that CMI really only had three verbs - those are verb-5, verb-6 and verb-7.
    • Already from the very early days there was a special "verb" defining the default action if a verb didn't have one. And a fallback script that would be called if there wasn't a default action on the object. E.g. having Guybrush say "That doesn't seem to do anything".
    • There was also a special verb for the "quick action". That one had a macro so that rather than writing something like verb get-verb { double-verb = open } for a door, you could just write quick-verb is open.
    • Assigning an icon to an object in the inventory was also done with a special verb, once again the actual verb code was hidden inside a macro - so, icon is bucket-icon, rather than verb get-icon { icon-number = bucket-icon }.
    • Similarly, in the CMI example, you'll see verbs like
      verb-216 {
         var-630[0] = "/PU_M048/climb through"
      }

      ... which defines the name for the "use" verb. That would likely, once again, be shortened by a macro, something like use-name is "climb through" - with a similar macro for the look verb (verb-217) and talk/eat verb (verb-218).

    • In other words, the verb scripts were really an object-oriented-programming-like way to have "methods" on an object - not just for the actual verb actions.

  • start-script got two flags, also there relatively early in SCUMM's evolution: bak for "background scripts" - while scripts would generally run until they were stopped, in a multi tasking way (break-here, wait-for-* etc. would allow the engine to interpret all the other scripts currently running) - that didn't apply to cut-scenes. Whenever a cut-scene was entered, all other scripts were paused - except for scripts started with bak. That would be stuff like the most common script example: The grandfather clock going "tick tock" - don't want that to stop when a cutscene is running.

  • The other flag is rec, which stands for "recursive" but doesn't actually mean that... It's for scripts that should be allowed to run multiple copies at the same time. Not recursively, but in parallel. If you started a non-rec script that was already running, the original script would stop, and a new version of it would be started instead.

Edited by Serge
  • Thanks 1
Link to comment
Share on other sites

Posted (edited)

I get the idea of discrete ROOM objects (although when it's stored on a harddrive, not a floppy, there's less need to make everything self-contained in that way -- the speed of r/w access, copious available RAM and storage make them less necessary), I was more thinking of the way global objects are handled. If I was writing my own domain-specific language for adventure games today (making games of a similar scope to SCUMM games), I'd structure it differently -- rightly or wrongly.

 

Edited by ThunderPeel2001
Link to comment
Share on other sites

Yeah, I'm sure that Ron and Chip would originally have considered keeping actual globals separate from the rooms, but ended up with this for practical reasons. 🙂

Link to comment
Share on other sites

2 hours ago, Serge said:

The other flag is rec, which stands for "recursive" but doesn't actually mean that

 

is it certain that "recursive" is meant? Could it be "recurring" or some other more fitting term?

Link to comment
Share on other sites

Posted (edited)

To be sure, the meaning is never documented in actual C code for SCUMM or SPUTM. It's always just shortened to "rec" there, with "recursive" showing up in unrelated places where it fits.

 

However, in any and all SCUMMlet documentation of the language - from around MI2 all the way to CMI, the flag is consistently referred to as being used to run the script "recursively". 🙂 The flag (as well as the "bak" flag) was added for Last Crusade, as far as I recall - and to be fair, it may have originally been intended for a specific recursive script in that game. Even if the support for actual recursion is limited in SCUMM: Although it was just a measure for memory management and could be changed by just changing a constant and recompiling SCUMM and SPUTM, the maximum number of active scripts at a time was typically limited to 20-30 scripts - and multiple "rec" scripts would count towards that limit.

Edited by Serge
  • Like 1
Link to comment
Share on other sites

Posted (edited)

Just one more, to celebrate that I just got state annotations [ON/OFF vs OPEN/CLOSED vs HERE/GONE] mostly working - and because it shows off quite a bit of the more advanced features of early SCUMM. The Maniac Mansion boot script (including the demo):

 

https://gist.github.com/Jither/bdb253854c6938675e9d525157e0b3fb

 

Worth noting that the setup of the actors wasn't there in the C64 version - where the actors (and quite a few other things) were actually hardcoded. Yes, these bit array assignments:

 

room-always-lit[44] = 1

 

... should actually eventually have "44" replaced by the room name. That requires that I set up typed array items first. 🙂 (now fixed)

Edited by Serge
Link to comment
Share on other sites

Posted (edited)

Awesome, thank you for this amazing analysis

This is actually very similiar to what I've tried doing in my project (nutcracker) also.

(it's available here in case you would like to have a look:

https://github.com/BLooperZ/nutcracker#decompile-game-script)

which is currently able to parse v3-v8 (+ humongous entertainment games) (v3 and v4 currently not exposed in CLI)

my sources were the documents published by Aric Wilmunder and the windex-enabled demos (MI2 + Putt Putt)

 

I still working on figuring how to correctly transform the control flow to structured (specifically case..of and if..else)

 

also, I would like to add some more points I first learned from the short SCUMM code sample included in FT remastered (IIRC)

(you might already saw this, but they weren't written here yet)

- Indentation was done with TAB characters

- Comments are added with semicolon

 

Also, I haven't managed to figure out how the instructions for using colon, plus and comma in say-line until your example, so thanks again.

 

Edited by rzil
  • Like 3
Link to comment
Share on other sites

Posted (edited)
On 7/3/2023 at 6:20 PM, rzil said:

This is actually very similiar to what I've tried doing in my project (nutcracker) also.

(it's available here in case you would like to have a look:

https://github.com/BLooperZ/nutcracker#decompile-game-script)

Looking good! 🙂 Haven't seen it before - but then, haven't searched for SCUMM on github recently. 😁

 

This might or might not be helpful - the current output for MI2 Woodtick with annotations for variables, actors, objects, etc. - since that's something that's supported by both nutcracker and my decompiler, "notes can be compared" 🙂

 

https://gist.github.com/Jither/077e9e90ad4d4f30127f2eddc3730fdb

 

Can't show the original script, obviously, but can say what the decompiler is missing in that output:

  • Note that by default, the decompiler outputs Unicode that remaps characters to what they would look like in Brief (codepage 437). Hence, bytecode 7 as seen in the dialog choices is output as a bullet.
  • It doesn't yet recognize the type of bit-variables - e.g. housecall-happening = 0 would normally be written housecall-happening is true - fixed
  • It doesn't yet name (or allow annotation at all) for local scripts and local variables (or script parameters) - hence, anywhere it says e.g. loc-0, in addition to not having a proper name, it also won't be able to properly type the content of that variable. For example, in woodtick-music-control, the first parameter, loc-0, is the previous room. and loc-1 is the new room. Hence, all the numbers they're compared to should also eventually be room names rather than numbers. ETA: Local scripts, variables and parameters are now named.
  • As mentioned earlier, all the iMUSE commands are sound-kludge commands wrapped inside macros with proper names. Since the decompiler doesn't do "reverse macro expansion" yet, those look unwieldy.
  • An alias for default in case statements is otherwise. The decompiler does allow to pick that in its settings.
  • It doesn't yet declare local variables at the top of scripts (but your decompiler already does that 😁) ETA: Now it does.
  • I haven't annotated the possible values for e.g. graphics-mode - e.g. 19 is VGA.
  • I also haven't annotated non-standard chores, since they'll mostly vary between costumes (and I haven't implemented a nice way to annotate chore-per-costume)
  • For some reason, the "modern" (non-MM) version of class-of doesn't properly infer the return type (resulting in "134", rather than "LOCKED" - yes, the pegleg can be LOCKED 😉)
  • Another example of macro expansion - the set-dialog macro that I mentioned in another thread (and which is demonstrated in the VGHF interview with Ron), is this (set-dialog 1 "Then who keeps up the law and maintains order?"):
    foo = (120 + 1 - 1)
    verb foo
       at 0,dialog-ypos
       name "•Then who keeps up the law and maintains order?"
       on
       key dialog-key
    dialog-ypos += 8
    foo -= 120
    ++dialog-key
    ++dialog-lines
    say-screen-escape[foo] = 1
  • In this case, the wait-for-dialog macro is used in one case (after "Is this some sort of bribe situation?"). But not in the other (after "Then who keeps up the law...") Because using the macro in the latter case would require the animation resets for Largo and Guybrush to be written into each case. The macro doesn't allow "shared" code between choices. As mentioned in another thread, the jumps are how dialog was generally handled. Once again, the macro isn't "un-expanded".
  • For some reason, my refactor code misses a nested if in one spot and keeps it as a jump. It may be due to the decompiler having a limit setting on various types of nested code. This because jumps in huge dialog trees can often be interpreted as a huge amount of nested loops - but shouldn't be - they were originally just jumps, and the nested loops actually make things less readable. 😁
  • The annotated names used in this output may or may not bear some resemblance to the names in the original script.
  • The empty script-148 was actually additional code for the music, which was dropped before MI2 was released.

 

 

Quote

I still working on figuring how to correctly transform the control flow to structured (specifically case..of and if..else)

 

 

Haven't looked closely at the Python code, but probably the best way to transform control flow without pulling too much of your hair out is to create an actual abstract syntax tree from the bytecode and transform that. 🙂 But again, not sure if that's what you already do.

 

One thing to be aware of when transforming flow is that jumps from one verb into another wasn't uncommon in early games. 🙂

 

Quote

- Indentation was done with TAB characters

 

Not always 🙂 - in all games before FT (as far as I recall), the indentation convention was 3 spaces, and dialog lines were indented to align exactly.

 

Quote

- Comments are added with semicolon

 

As a fun addition to that: C-style multiline comments /* ... */ were also supported, but very rarely used. Another very rarely (if ever) used feature is that, in addition to decimal and hexadecimal (0x1a), it also supported binary (0b010), octal (0O137), and quaternary (i.e. base 4 - 0q103) numeric literals. 😁

 

Edited by Serge
Updates
  • Like 1
Link to comment
Share on other sites

This is so interesting! Thanks a lot for sharing.

 

Scumm Revisited 5 was really useful to me when I was developing a JS replica of the cannon mini-game in CMI. I was able to understand most of the logic by looking at the scripts. The only thing I wasn't able to properly understand was the code to handle the position and size of the cannon balls. Without proper variable names, I couldn't really make sense of it. In the end, I just used an array with all positions and sizes I measured manually. Not very elegant, but worked well.

  • Like 3
Link to comment
Share on other sites

Posted (edited)

Yeah, SCUMM Revisited 5's decompiler was very hacky and quite unreliable. If I had the documentation and insight I have now, SCUMMRev in general would have looked very different (but probably also have been less fun to make). 🙂

 

I'd like to eventually release the decompiler as open source, but right now it still has comments and annotations (like the ones used for the MI2 example above) which could be... troublemakers. 😁 It also decodes art to LBM (even in CMI where LBM wasn't used anymore), rooms to FLEM, and costumes to BYLE format (no support for CYST yet). But the latter obviously aren't very useful for the general public, unless the tools are somehow released one day.

 

And I love that cannon replica! 😁

 

 

Edited by Serge
  • Thanks 4
Link to comment
Share on other sites

18 hours ago, Serge said:

I'd like to eventually release the decompiler as open source, but right now it still has comments and annotations (like the ones used for the MI2 example above) which could be... troublemakers. 😁 It also decodes art to LBM (even in CMI where LBM wasn't used anymore), rooms to FLEM, and costumes to BYLE format (no support for CYST yet). But the latter obviously aren't very useful for the general public, unless the tools are somehow released one day.

 

 

Looking forward to that! :D 

  • Like 1
Link to comment
Share on other sites

Posted (edited)
2 hours ago, AndywinXp said:
Quote

unless the [official] tools are somehow released one day.

Looking forward to that! :D 

 

So do I. 😉

 

The Woodtick script above has been updated with local variables and parameters named - a new decompiler feature. It still doesn't apply types to the arguments where the scripts are called - that's on the TODO list (meaning start-script bak woodtick-music-control 7 12  would turn into start-script bak woodtick-music-control woodtick inn)


The only other major thing that's missing name and type annotation support now are the local variables (actually parameters) in verbs (ETA: oh, and labels, which I keep ignoring)

Edited by Serge
  • Chef's Kiss 1
Link to comment
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
×
×
  • Create New...