Community
SCI Programming => SCI Syntax Help => Topic started by: Cloudee1 on May 07, 2015, 11:10:50 PM
-
So it's time to start seriously looking at this coding business and trying to sort it out. The latest decompilation that has been made available has been SQ4, so I am starting there. And not only starting there, but actually starting at the beginning... with the intro script.
I have been doing some tinkering with the code layout and whatnot to actually get it as recognizable to me as possible. I figured I would start throwing out some rudimentary questions as I come across things I am not sure about as well as some clarification just to make sure what I think is happening is actually what is happening. So I guess to start with, I will post the code that I am actually staring at. I commented the hell out of it so it might actually be easier to read if you copy it and paste it into our current version of sciCompanion just for the syntax highlighting.
//******************************************************************************
// SQ4 - Space Quest Splash Screen and initial Intro bit
//******************************************************************************
(include "sci.sh")(include "game.sh") (script 1)
//*****************************************************************************
(use "Main") // Good old main script, we are used to this one...
(use "SQRoom") // Seems to mainly handle ego motion
(use "n958")// Seems to be a small load / dispose script...
(use "User")// We are used to User
(use "Obj")// We are used to Obj
//*****************************************************************************
(local
local0 // unused by this script so really could be omitted
local1
)
//*****************************************************************************
(instance public rm001 of SQRoom
(properties picture 106) // picture 106 is the sierra splash screen...
(method (init)
(super:init())
(self:setScript(RoomScript))
(self:setRegions(707)) // actually the intro.txt file... holds the handleEvent for the intro screens
Load(rsPIC 1)// Bah, who needs Load... no one ever uses it... anyway, loads pic 1 into memory
proc958_0(135 68 69) // found in script 958... the small load / dispose script
// So if I am halfway right, this will load param1 (resource type) and then
// loop through the remaining paramaters representing the resource numbers
// of that type... Loading each one in succession
// looking at this in viewer, 135 must represent RSFONT, and would therefore load
// fonts 68 and 69 (there aren't any other resource types which include 68 or 69)
// so 135 must mean Font... we are used to this (define rsFONT $87)
)// end Method init
)// end Instance
//******************************************************
(instance RoomScript of Script
(properties)
(method (doit)
(super:doit())
// So we know do it checks things every cycle...
// so we are looking every cycle to see if the changestate is at state (or case) 1
// and the music being played is throwing prevSignal 10... not sure what prevSignal 10
// means but anyway, when those two conditions are met, then the RoomScript is supposed
// to cue itself. Which I have never tried it but judging by this, it will bump the
// changestate method on to the next state (case) as I am not seeing anything else here
// that does do that.
(if ((== state 1) and (== (send gLongSong:prevSignal) 10))(self:cue()))
)// end Method Doit
(method (changeState newState)
(var temp0[3]) // unused by this method so really could be omitted
(switch (= state newState)
(case 0
(send gLongSong:loop(-1)playBed()) // Starts the song that doit is keeping an eye on
= cycles 2 // waits a couple of ticks then moves on to case 1
)
(case 1) // just hangs... This threw me until I noticed the cue() in the doit... thats what moves us on
(case 2
(send global2:drawPic(1))// Not sure what global2 is... gRoom maybe, we have always just used DrawPic(###) before
// anyway, this throws up the Space Quest Title Screen
= register 200 // I see register is used in SQRoom.txt but in the context of newRoom... not sure what we are registering here.
// But judging from the next case, it looks like this is going to be used as a quick and dirty timer variable
= cycles 2 // waits a couple of ticks then moves on to case 3
)
(case 3
Animate((send gCast:elements) 0) // I am not sure what this actually does... do we have a cast?
(while (--register and (== gTheGNumber gNumber))// so while register > 0 ?? I don't get what kicks us out of the while loop
(if (== register 150) // when the register variable is at 150... started in case 2 at 200
= local1 proc0_12(" 1991 Sierra On-Line, Inc." 67 1 177 70 316 28 proc0_18(global157 global156) 29 gColor 30 1 33 68)
// ok so not sure why this had to have a name, local1 is one of this scripts local variables defined at the top.
// local1 is set to proc0_12 which is pretty obvious is some sort of display command. as it throws the copyright
// line on to the screen... not sure why there are processes inside this display command though or what all of those parameters
// represent.
)
(User:doit()) // not sure why this is here....
Palette(6 160 191 -1) // or these
Palette(6 128 159 -1)
)// end while
(send global2:drawPic(803 30)) // Ok so this I can tell you, draws picture 803 on the screen. Again not sure about the send bit with DrawPic
(if (== gNumber gTheGNumber) // hmm... so apparently if these two match
(send global2:newRoom(6))// then it is time to take this intro on to room 6... also, this confirms global2 = gRoom as we knew it previously
// (send gRoom:newRoom(###))
)
)
)// end switch
)//end method
)// end instance
So what do you think, is this something we want to try and sort out together or no... I just figured we would take the game as it comes and look at the code and decipher what it is actually doing on screen.
-
A few notes:
- If I recall correctly, it's generally better to use gRoom:drawPic, and not the DrawPic kernel. If you use DrawPic, and the game is saved and restored, anything you drew with DrawPic will no longer be there.
- You're kicked out of the while loop after 200 iterations, when register reaches 0. I dunno what gNumber is, you'll have to follow it to see why it got named that.
- Palette(6 ...) is palette cycling. Looks like it's animating two separate regions of the palette (128-159 and 160-191).
-
That would be the SQ4 logo which cycles the background and text separately. I posted an example of that a while back in the "Possible new version of SCI Companion" thread.
-
The only problem I have with that is that it is in the final case statement of the room. It is in this same case that the room moves on to room 6. Sierra Logo is long gone by then, It goes away in case 2. So if there is picture pallette cycling going on, shouldn't it be happening well before then.
-
The Sierra logo (which doesn't cycle) is gone, but it is replaced by the "SPACE QUEST IV" title logo, which is the one that cycles. Seems to match what happens in the room exactly.
-
That is so weird... I would have sworn last night that the Sierra Logo was cycling too... I think I might just be losing it lol :o
-
It cycles in SQ5!
-
Ok, so now that I am over that...
On to this prevSignal value in the doit check... (== (send gLongSong:prevSignal) 10)
I have thrown some values into scicompanion to see if I can replicate and I am not finding anywhere that prevSignal is ever actually changed... or reached by the normal playing of the sound.
So in the titlescreen, I have included a changestate and thrown this into case 0
(case 0
(send gTheMusic:
prevSignal(0)
stop()
number(900)
loop(-1)
play()
)
= cycles 2 // waits a couple of ticks then moves on to case 1
)
and then I have changed the doit method
(method (doit)
(super:doit())
(if(== state 1)
(if(== (send gTheMusic:prevSignal) 10)// Doesn't ever seem to occur...
(self:cue())
)
)
)// end Method
If I comment out the check for prevSignal 10, then the changestate does move on past state 1, but with the line included it does not. What, where and how does prevSignal get set I have scoured both our current template and the sq4 decompile and I am simply not finding anything. This would actually come in handy for my own splash screen and the falling bag trigger.
*Edit
So I got a little peeved at this and decided to try something. I made a quick little timer that printed out the value of the prevSignal every so often. It apparently never changes on its own. A little more desperate I went ahead and decided to force it to change.
I added a couple of local variables
temp0
showMe = 50
and then in the doit method I changed it to this.
(if(== state 1)
// Fake it to see what it does...
(if(> showMe 0) --showMe)
(if(== showMe 0) // when it reaches 0
= temp0 (send gTheMusic:prevSignal) // set our temp0 variable
FormatPrint("prevSignal %3d" temp0) // and show it to me
= showMe 50 // reset the timer
(send gTheMusic:prevSignal(+ (send gTheMusic:prevSignal) 1)) // and force change prevSignal
)
(if(== (send gTheMusic:prevSignal) 10)// Doesn't ever seem to occur on it's own...
(self:cue())
)
)
-
It's probably triggered via MIDI from the sound resource itself. MIDI files (and by extension, sound resources) have various CC controllers that various MIDI devices can read and understand as various things such as an instrument change, tempo change, pitch shift, pan change, volume change, etc. SCI also recognizes a specific CC to cue a script event. I believe Ken Allen said in one podcast that it was CC controller 127. That's used for things like cuing the next sequence in a cutscene for instance once a song reaches a certain point. Like in the SQ4 intro where the logo "Roger Wilco and the Time Rippers" won't appear until the full theme starts playing. If you swap the intro theme for another song resource that has no scipt-cuing CC controller in it, the intro will just sit there at the Sierra logo and never go beyond it.
-
It's probably triggered via MIDI from the sound resource itself. MIDI files (and by extension, sound resources) have various CC controllers that various MIDI devices can read and understand as various things such as an instrument change, tempo change, pitch shift, pan change, volume change, etc. SCI also recognizes a specific CC to cue a script event. I believe Ken Allen said in one podcast that it was CC controller 127. That's used for things like cuing the next sequence in a cutscene for instance once a song reaches a certain point. Like in the SQ4 intro where the logo "Roger Wilco and the Time Rippers" won't appear until the full theme starts playing. If you swap the intro theme for another song resource that has no scipt-cuing CC controller in it, the intro will just sit there at the Sierra logo and never go beyond it.
This is true in concept, with the addition that the exact mechanics changed throughout the SCI timeline.
-
Yeah. I imagined it would have.
-
Ah, so if I were to open up one of my midi sounds in soundbox and actually set some cue points, then I might be able to see this in action... Gonna have to see what I can get to happen.
Holy shit, that kicks ass. ;D How have I never known about this before.
So... now theoretically, I can create a silent midi. Then play that sound whenever I want while simultaneously kicking off some mp3's via sciaudio and still halfway be able to trigger musically timed events regardless of cycles or seconds... hell, timed events in general, regardless of sciaudio
and yes, I am still talking about SCI0 here.
-
Ok... so I guess that takes care of the major questions concerning the first room of SQ4 intro. On to the second room of the intro room 6.
Now this screen shows us a space scene with a ship that flies across it, the game's subtitle, and then a handful of credits before moving on to room 9.
I have managed to get as far as getting the ship to fly across the screen, but I haven't been able to stop getting the ship to fly across the screen.
In the use section, it calls for a script called MCyc. Now for the most part I got this class into the template game and have been able to call it. Currently in my set up, it is script 970. There was a public procedure that it called, so I just went ahead and copied it out of the main script and stuck it straight into the script. Also, I went ahead and added global37 to the main script. That was pretty much all I had to do to get this script in the sci0 template and compiled.
/******************************************************************************/
(include "sci.sh")
(include "game.sh")
/******************************************************************************/
(script 970)
/******************************************************************************/
(use "Main")
(use "Cycle")
(use "Obj")
//*****************************************************************************
(local
)
/******************************************************************************/
(procedure public (proc999_6 param1 param2)
return | StrAt(param1 (* 2 param2)) (<< StrAt(param1 (+ 1 (* 2 param2))) $0008)
)
//******************************************************************************
(class MCyc of Cycle
(properties
client 0
caller 0
cycleDir 1
cycleCnt 0
completed 0
value 0
points 0
size 0
)
(method (init theClient thePoints theCaller theCycleDir)
(var temp0[2])
= client theClient
= points thePoints
(if (>= paramTotal 3)
(if (>= paramTotal 4)
= cycleDir theCycleDir
= caller theCaller
)(else
(if (IsObject(theCaller))
= caller theCaller
)(else
= cycleDir theCaller
)
)
)
= size 0
(while (<> proc999_6(points size) 32768)
++size
)
(if (== cycleDir 1)
= value 0
)(else
= value (- size 4)
)
(super:init())
)
(method (doit)
(if (> ++cycleCnt (send client:cycleSpeed))
= cycleCnt 0
(self:nextCel())
)
)
(method (nextCel)
(send client:
loop(proc999_6(points value))
cel(proc999_6(points + value 1))
x(proc999_6(points + value 2))
y(proc999_6(points + value 3))
)
= value (+ value (* cycleDir 4))
(if (((== cycleDir 1) and (>= value size)) or ((== cycleDir -1) and (< value 0)))
(self:cycleDone())
)
)
(method (cycleDone)
= completed 1
= value 0
(if (caller)
= global37 1 // added to main script
)(else
(self:motionCue())
)
)
)
Now in room 6, there is a local variable which is one long ass array. This array gets sent to this motion class. It was all on one line, I went ahead and added some line breaks to try and make it a little easier to follow. Assuming I have it right, we end up with this.
// cycle Dir, Cel, x, y
falconLocations[50] = (1 0 114 70
1 0 116 70
1 0 118 70
1 1 119 70
1 1 123 70
1 2 133 70
1 2 142 70
1 3 162 71
1 4 197 71
1 5 239 71
1 6 314 72
0 0 65436 65436 32768 )
Then of course, there is our view. In this case they have it set up as a prop which I thought was pretty interesting
(instance falcon of Prop(properties view 901))
And then finally in the changeState, our prop uses this new motion class as well as the local variable we talked about.
(case 6
(falcon:
setPri(8)
z(0)
setCycle(MCyc @falconLocations self)
)
)
(case 7
Print("Hi")
(falcon:dispose())
)
So in theory, this prop should run through the motion class MCyc based off the variable points defined, and when complete, cue itself to the next state in which it gets promptly thrown away. I am again missing something here, As I can not get it to recognize that it has completed the cycle through the variable points. Instead, the view reaches the last point (presumably) and then jumps back to the first point and cycles through them again. Similar to setting setCycle(Fwd) instead of End.
-
Ah I was thinking that cued CC controllers wouldn't be possible with sciAudio. That's neat. :)
I really wanted to delve heavily in what could be done with this method of cuing scripts and vice versa to sound resources, but music always comes at the end of a project's development and I never reached the end of KQ2SCI. All this is exciting me and making me want to get back to it. However, I have a fairly busy touring schedule coming up starting next month.
-
fyi, the new version of SCI Companion I have on github has midi import support for sounds, and you can add cue points and the loop point (I don't think the old version of SCI Companion had this, did it? I forget). It's only SCI0 though. The sound format changed in SCI1 and I haven't written support for that yet. And Lars mentioned the way cue points worked has changed throughout SCI, so that's not fun :-(.
-
:)
-
Ok, so I now have this issue resolved and another official motion class added to my game.
global37 should actually point to gCastMotionCue, so changing the cycledone method at the end of the MCyc class to use the correct global variable
(method (cycleDone)
= completed 1
= value 0
(if (caller)
= gCastMotionCue 1 // = global37 1
)(else
(self:motionCue())
)
) // end method
Ship now successfully crosses the screen... On to discover the next issue.
-
Fantastic work, Cloudee. At this rate you can recreate SQ4 in SCI0! I'm sure Scott Murphy would appreciate that lol.
-
Ok, so I now have this issue resolved and another official motion class added to my game.
Great job!
To clean it up, you might want to use the hex equivalent for the 32768 instead, so it looks more like a "flag": $8000. I guess this is a signal that marks the end of the list of cels/points.
And the two 65436 in the array should be -100, so it's clear it's just moving it off screen.
One the problems with decompiling is trying to guess whether a number is signed or not. When I encounter a 65535, it's almost certain this was a -1 in the original source code, so I special-case that.
The unsigned range (32768, 65535) corresponds to the signed range (-32768, -1)
What do you think the M stands for? MultiCycle? MultiCel?
-
Fantastic work, Cloudee. At this rate you can recreate SQ4 in SCI0! I'm sure Scott Murphy would appreciate that lol.
Then you could re-add the parser that they wanted for the game.
-
That was what I was referring to. ;)
-
Great job!
To clean it up, you might want to use the hex equivalent for the 32768 instead, so it looks more like a "flag": $8000. I guess this is a signal that marks the end of the list of cels/points.
And the two 65436 in the array should be -100, so it's clear it's just moving it off screen.
One the problems with decompiling is trying to guess whether a number is signed or not. When I encounter a 65535, it's almost certain this was a -1 in the original source code, so I special-case that.
The unsigned range (32768, 65535) corresponds to the signed range (-32768, -1)
What do you think the M stands for? MultiCycle? MultiCel?
I was thinking Manual. As it requires that you place the posn points and cels via the array manually.
... ultimately my goal is to try and get the sci0 templates as close to what the sci1 template might be... that and I also like the idea of some added functionality right now. I have a couple of projects that are way to far along to even think about starting over with... and it just so happens that one of them does include sq4 in sci0... sq5 for that matter too. But this is a very long term project.
-
and it just so happens that one of them does include sq4 in sci0...
Nice! Don't forget that the backgrounds and sprites already exist in 16 colours in the EGA version. The VIEWs are even in SCI0 format and work in SCI0 games (tried it already) and the backgrounds can just be imported with Companion's current Pic importer tool.
-
Nice! Don't forget that the backgrounds and sprites already exist in 16 colours in the EGA version. The VIEWs are even in SCI0 format and work in SCI0 games (tried it already) and the backgrounds can just be imported with Companion's current Pic importer tool.
I may have to see if I can't track down a copy of that (http://sciprogramming.com/community/index.php?topic=654.msg3588#msg3588), because just getting the intro going I am not overly excited about the pics and views that are coming out. I was thinking that I was going to have to sit down and redraw them all... which is something I am loathe to do. That is the reason that Voodoo Girl has stalled, I have a number of rooms that I want to redraw but not the desire.
-
The only annoying part would be recreating the priority and control screens. Actually, no that would be easy. You can just export the priority and control screens in SCI Viewer as BMPs and import those with Companion's import tool. It'll draw everything in the visual field but you'd just need to go in and replace the visual palette selectors as priority or control. That would be a welcome feature to the new SCI Companion, however (importing priority and control masks).
-
fyi, the new version of SCI Companion I have on github has midi import support for sounds, and you can add cue points and the loop point (I don't think the old version of SCI Companion had this, did it? I forget). It's only SCI0 though. The sound format changed in SCI1 and I haven't written support for that yet. And Lars mentioned the way cue points worked has changed throughout SCI, so that's not fun :-(.
I don't know if you can do it, but a time line would come in really handy as well as the ability to right click the timeline for inserting cues. I have no idea how many ticks are in a second so adding cues has become a total guessing game. Right now everything works, but absolutely nothing is timed out correctly lol.
Also by the way, playing a midi sound with cue points via the interpreter while simultaneously playing an mp3 via sciaudio works like a charm. Especially since you can set cue points with different values all over the place means that the one sound file can cover a whole lot of distance.
-
So this stupid procedure has me scratching my head.
(procedure public (proc999_1 param1 param2)
= param1 (- param1 (* param2 (/ param1 param2)))
...
Unless I am mistaken, doesn't that ultimately breakdown into
param1 = param1- param1
-
Lol yeah it does. No matter what you put in for values for param1 or param2 it all results in a zero doesn't it?
-
It looks like it calculates the remainder. If param1 is 7 and param2 is 3, the result should be 1.
It's basically the same functionality as the % operator. I think Lars mentioned that the mod opcode changed behavior over different SCI versions in regards to how it treats negative numbers. Maybe this procedure is a replacement "fix" for the mod opcode when negative numbers are used.
-
Sir. I disagree ???
It looks like it calculates the remainder. If param1 is 7 and param2 is 3, the result should be 1.
= param1 (- param1 (* param2 (/ param1 param2)))
= param1 (- 7 (* 3 (/ 7 3)))
param1 = 7- (3(7/3))
param1 = 7 - 7
param1 = 0
I don't see any combination that will ever get us anything but 0 for a resulting param1, except for maybe an initial 0 value, but wouldn't it just be easier to check for that specifically and then just reset param1 to 0 without bothering with the operators.
-
I don't follow this step:
param1 = 7- (3(7/3))
param1 = 7 - 7
The parentheses define the order of operations. It goes like this:
param1 = 7 - 3 (7 / 3)
param1 = 7 - 3 (2)
param1 = 7 - 6
param1 = 1
This is pretty easy to verify for yourself. Stick this in a script:
(procedure public (proc999_1 param1 param2)
return (- param1 (* param2 (/ param1 param2)))
)
And then call
FormatPrint("Result is %d" proc999_1(7 3))
-
I guess I was thinking that the remainder would come out of 7/3 too. So then multiplying that result by 3 would negate the whole point of the division.
-
I guess I was thinking that the remainder would come out of 7/3 too. So then multiplying that result by 3 would negate the whole point of the division.
The result can only have one value, so how would the remainder and quotient both be produced? SCI has no floating point operations, just integer operations.
http://en.wikipedia.org/wiki/Modulo_operation#Modulo_operation_expression
for environments lacking a mod() function (note that 'int' inherently produces the floor value of a/n)
a - (n * int(a/n)).
-
In case anyone is wondering, this procedure is coming out of the sq4 obj script.
And I digress, the resulting print statement was in fact, 1
-
Ahhh yes. Floating point. Right.
-
Ok, I am staring at two different Motion classes... Approach and Chase. They are virtually identical. The only difference is in the doit method.
One of them includes this and the other one does not.
(if (== {b-moveCnt} 0)
(super:init(client (send who:x) (send who:y) caller))
)
Here is the whole doit method just for further reference
(method (doit)
(if ((self:onTarget()))
(self:moveDone())
)(else
(super:doit())
(if (== {b-moveCnt} 0)
(super:init(client (send who:x) (send who:y) caller))
)
)
)
What does that actually do. Reset the chaser's x and y to it's current position and trigger an instance presumably with the caller, if it isn't moving?
It looks like who is the chased and if I understand correctly, the client would be the chasing actor.
-
Well just from the names, I would assume Chase keeps going after the who. So once it gets to where they were, it reinitializes itself maybe?
Whereas Approach would go to the who and stop, and be done.
I forget what b-moveCnt is again, but it's something to do with the bresenham line algorithm that moves actors along paths.
-
Yeah, they both get to the target (within a defined distance) and stop. Literally the only difference is that b-count statement.
So I am just assuming that they changed the name of the script from one game to another. I think I prefer chase personally so that is probably what I am going to go with and ditch the other version. I just wasn't sure what that was actually doing
-
Which game are you looking at now? I don't see either of those classes in SQ4. In SQ5, they both exist. I'm pretty sure they are supposed to have different behavior, so I would hesitate before considering them "the same".
It's possible they behave the same in SCI0, but differently in SCI 1.1. According to the ScummVM code, the DoBresen kernel function updates {b-moveCnt} in SCI0 and SCI01, but not in SCI1.1 (presumably leaving it up to the scripts to update?). So the same code might have different functionality in different SCI versions.
-
Actually it looks like I am looking at approach and chase both from sq5. That is where the b-count difference is.
There is also an approach in the kq4 cycle script. I had thought that was where one of them had come from.
* Oh yeah, there was one other difference. But it is so pointless I didn't think it was worth mentioning. The default distances for approach was set to 20 and chase 0. But as that is one of the passable parameters, it's a worthless difference.
-
Ok, so it looks like b-moveCnt just counts down from the moveSpeed number of the client to zero (or vice verse, doesn't matter). At any rate, it should be 0 every time the client moves.
Therefore Chase is constantly re-initializing itself every step the client takes.
So that suggests that the difference between Chase and Approach is that Chase adjusts for the who moving around, while approach just "captures" the who's initial position when you set the Approach motion. Is that what you observe?