Community
SCI Programming => SCI Development Tools => Topic started by: troflip on April 30, 2015, 02:11:54 AM
-
I've gotten to an important milestone I think :D
I've attached a zip file with 210 decompiled script files from SQ5. I renamed the extensions to .txt (instead of .sc) just to make it easier to browse them in Windows.
A few notes:
- I've made no attempt at compiling these files (in fact the code that generates SCI1.1 script resources (with the separate heap/scr) is still not implemented). So you may see stuff that certainly looks like it won't compile. An example is having strings in a script's local vars. SCIStudio doesn't allow this, but it's required for decompiling scripts, really. So I'll need to add this syntax support.
- Almost all the variable naming (and all the file naming) was done automatically, with no interaction from me. So it is pretty much just "click and go" to get the results you see. Of course, you'll see that exported procedures have nonsense names, so there will need to be some interaction from the user to supply names to these functions based on what it looks like they do (and then the next recompile will pick up those changes).
- I did a quick search for ERROR in these files, and I found a handful of places where this appears. This is indicative of some problem with resolving symbols, so I need to look at those cases
- There are still lots of issues with detecting the higher level control flow structures, and making sense out of some code. So I often have to fall back to asm. So you'll see some asm in there, but not *too* much. I put in some stats calculations, and the result of decompiling the entire game (which took 9 minutes) was:
Decompiled 3079 of 3121 functions successfully (98%).
Overall bytecount success rate: 96%.
So that's really not too bad, I can "successfully" decompile roughly 95% of the code. I say "successfully", because the only validation I can do now is when my decompiler logic runs into things it doesn't know how to handle. In the 95% "successful" code, there still may be a lot of it that is incorrect. So there's still lots of work to do.
- There are a few turds leftover from previous compilation attempts (for example you'll see n000.txt, which was an old version of main.txt (before I wrote the code to auto-detect script filenames), and it's full of asm). I'm too lazy to re-upload the zip.
- I still have bugs in the formatting of the code output, so parentheses may be in weird places.
-
Nice. Is that all of them?
-
Yup.
-
This is just too cool. I'll be mulling over all of this for sure! Great work, troflip! This is exciting!!
I have a request, if it's not too much trouble. Is it possible you could provide the complete source for SQ4 or at least the room script for the Time Pod (script 531)? (either CD or disk, it doesn't matter) There's a long-standing easter egg mystery in the time pod room that has stumped a lot of SQ fans for a long time on the SQ.net forums. Having the source would really shed a lot of light. Specifically, we were wondering if the "Blue frogs on my shoulder make me hap-py" line is actually triggerable in the game and if so how.
-
I notice that the obj script, as well as dcicon both "use" themselves... There are a few that do that and some that don't.
-
I notice that the obj script, as well as dcicon both "use" themselves... There are a few that do that and some that don't.
Cool, I've fixed that. Thanks for any bug reports!
I have a request, if it's not too much trouble. Is it possible you could provide the complete source for SQ4 or at least the room script for the Time Pod (script 531)?
Attached... have at it!
-
Thank you so much!
-
About the blue frog stuff, it is not possible to trigger that response. The frog object is a Feature, that is to say, a part of the pic that responds to commands (without being a view). A Feature usually has some sort of graphical representation, but in other cases one must resort to pixel hunting (there's a debug feature in LSL5 as welll). In this case, the Feature is placed on Roger's left shoulder, as the text implies (or rather, it is his shoulder).
Now, the reasons why you can't get the message in the released version:
- The frog feature is not initialized at room start-up, so the game system doesn't know it's there
- Even if the frog feature was initialized, another feature (restOfPod) takes precedence over it, so the game system doesn't care it's there
- The doVerb method of the feature disregards which icon was used - and doesn't call its superclass - so the default handler for the "look" cursor isn't run.
A little ScummVM-fu can solve the first two problems and enable the teleport code (indeed, I've done so). A hex-editor might solve the third and cause the message to be displayed.
EDIT: QfG2 used "suck blue frog" to enable the debug mode. I wonder what it is with Sierra and blue frogs...
EDIT2: There's a bug in the decompiler at the Print statement in that doVerb handler. There's supposed to be buttons with it, described via extra arguments to Print.
-
Well, I can't say that's not disappointing :(. However, apparently in NRS's SQ4 patch he re-enabled the "Time-O-Matic" which is attached to the frog instance. But I can't get it to work...care to take a look at his script with your decompiler?
EDIT: I guess attaching the script would have been helpful...I don't know if you need the HEP file as well, but here it is.
-
EDIT2: There's a bug in the decompiler at the Print statement in that doVerb handler. There's supposed to be buttons with it, described via extra arguments to Print.
Ugh... that's old code that I had to automatically pull stuff from text resources when I did my first stab at the decompiler back when... it special-cases the Print procedure... Thanks for pointing that out! So most of the Print statements in the decompiled scripts I provided will be wrong :P
Here's the corrected doVerb;
(method (doVerb)
(var temp0)
= local2 Print(531 16 30 1 80 "Time-O-Matic" 81 "Estros" 6 81 "Mall" 12 81 "Xenon" 18 81 "Ulence" 24 81 "Ortega" 0)
(if ((< global128 2) and (== local2 6))
Print(531 17)
)(else
(if ((< global128 2) and (== local2 12))
Print(531 18)
)(else
= temp0 0
(while (<= temp0 5)
(send newDisplay[temp0]:
loop(2)
cel(theGNewDisplayCel_2[+ local2 temp0])
)
++temp0
)
(hand:posn(180 180))
(send global2:setScript(timeToTimeWarpS))
)
)
)
To make for nice decompilation, I'll stlll need to provide some special casing for Print calls to interpret the selectors correctly. But these will presumably be different for each game, since it depends on the code in the Print procedure. From what I recall seeing, I think SCI1.1 games use a Print class that has more "strongly-typed" options for printing.
-
Is it possible you could provide the complete source for SQ4 or at least the room script for the Time Pod (script 531)? (either CD or disk, it doesn't matter)
If we're taking requests, I'd like to request KQ4 if that's possible yet. I know it's early SCI, but if its possible, that would be great. I've been keen to see Room 7 in KQ4 for a while. It's the one that is discussed in The Official Book of King's Quest.
I've been wondering for a long time whether they used conversion tools to convert between the AGI and SCI version of KQ4. Maybe comparing the two sources would answer this question. If migration tools were used, then we'd expect the KQ4 source to take on more of an AGI sequential script layout and not take much advantage of the OO capabilities of SCI. AGI didn't have loops, so a dramatically reduced number of loops might be a clue. Obviously they would have tidied it up after conversion, but there should still be some evidence in there.
Does anyone know if the AGI version of KQ4 pre-dated the SCI? What I mean is do we know if they wrote the AGI version first and then the SCI? I realise they were probably released at the same time, but what I was wondering is if the development phase of KQ4 started with the AGI version and then whether they migrated that to the SCI? Or did it happen in the other direction?
Maybe the KQ4 source would answer some of this.
-
Here's the KQ4 source. The decompiler had a hard time with the big functions in Main.
I'll see if I can get that SQ4 531 patch decompiled, but currently the decompiler is not looking at patch files (the way I had it set up, there was a chance it could grab the .hep file from the original package, but the .scr from a patch (which is disastrous), and disabling patch files was a (temporary) easy way to get the decompiler working on a full game).
-
Thanks for that. At first glance looking over the room 7 scripts, it doesn't look all that similar in structure to the AGI version. If they did use some sort of conversion tool, they must have done a lot of post clean up and refactoring afterwards.
-
Would they have been doing a lot of conversion/refactoring for the first SCI game? I may be wrong, but I thought that KQ4 was purposely written for SCI and the AGI was co-developed.
-
Well, I can't say that's not disappointing :(. However, apparently in NRS's SQ4 patch he re-enabled the "Time-O-Matic" which is attached to the frog instance. But I can't get it to work...care to take a look at his script with your decompiler?
EDIT: I guess attaching the script would have been helpful...I don't know if you need the HEP file as well, but here it is.
Hmm, I'm unclear how to use these. SQ4 is not a SCI1.1 game, so there aren't separate heap/script resources - and the naming is wrong (i.e. .scr instead of script.531). I tried sticking it in SQ5, and was able to decompile it, but it looks a bit messed up.
-
If the patch is the one that I am thinking if, it is a patch that adds the diskette version's graphics to the MPC release. If it is, it uses the CD's interpreter, which is SCI1.1.
-
Would they have been doing a lot of conversion/refactoring for the first SCI game? I may be wrong, but I thought that KQ4 was purposely written for SCI and the AGI was co-developed.
I don't know. You're probably right. The AGI version of KQ4 was quite an investment for something that probably didn't sell very well. It might even be the pinnacle of AGI development. If they focussed on one and converted to the other then it might have saved a lot of time. But maybe they developed them separately.
-
Collector's correct. It's the CD version which is SCI1.1. However, I discovered that his patch does not include the Time-O-Matic time warp after all, it was another script written by someone called the "Time Pod Debug patch" available on the archived SQ.Net website. So I sent you the wrong one anyway. Here's where you can get it. http://www.spacequest.net/archives/sq4/cheatdebug/
-
Phil, I assume you have noticed this already but just in case, I figured I would point it out too
Case statements... they seem to include an extra set of parenthesis inside the actual cases. This came out of room 100 but it is looking like it is the same throughout
(case 0
(
(send gSq5Music1:number(1001)loop(1) play())
(sparkle:init())
= seconds 4
)
)
-
Collector's correct. It's the CD version which is SCI1.1. However, I discovered that his patch does not include the Time-O-Matic time warp after all, it was another script written by someone called the "Time Pod Debug patch" available on the archived SQ.Net website. So I sent you the wrong one anyway. Here's where you can get it. http://www.spacequest.net/archives/sq4/cheatdebug/
Ok, that one worked. The only difference is that the (cables:init()) has been replaced by (frog:init()). It's difficult to change the lenght of a script if you're just directly editing bytecode, so it makes sense that he had to "replace" one object with another.
-
Ah interesting. I imagine that the line still won't work, however, since restOfPod supercedes it, right?
-
Phil, I assume you have noticed this already but just in case, I figured I would point it out too
Case statements... they seem to include an extra set of parenthesis inside the actual cases. This came out of room 100 but it is looking like it is the same throughout
Cool, thanks! I hadn't noticed (maybe because it was actually random if it happened - uninitialized memory). I just made a fix.
-
Ah interesting. I imagine that the line still won't work, however, since restOfPod supercedes it, right?
No, this should work just fine. The initialization order goes like this (excluding some more complex initializations that come before these, and things that aren't features):
(rogerHead:init())
(keyDisplay:init())
(mainScreen:init())
(smallCompartment:init())
(largeCompartment:init())
(headRest:init())
(cables:init())
(entryPad:init())
(exitButn:init())
(restOfPod:init())
What happens is that these go into an ordered list. If frog is substituted for cables, then it will indeed come before restOfPod. What I was doing was I added it manually by calling frog::init from the ScummVM debugger, in which case frog goes in last. In that case, restOfPod must be removed for things to work.
-
Ah. Strange, then, as I still can't seem to trigger it...
-
Here's a question, I found this global procedure in the SQ4 Main script. I'm trying to figure out what another action does elsewhere in the game as it bases what it does on whether this procedure returns 1 or 0. I understand that the & operator is comparing two things and if their bits both equal 1 it returns 1, but I don't know what's going on between those two values, what they resolve to, or what they're supposed to represent. I also don't know what global114 is supposed to be. Any ideas?
(procedure public (proc0_6 param1)
return & global114[(/ param1 16)] (>> $8000 (% param1 16))
)
Two other global procedures utilize this code as well.
-
Here's a question, I found this global procedure in the SQ4 Main script. I'm trying to figure out what another action does elsewhere in the game as it bases what it does on whether this procedure returns 1 or 0. I understand that the & operator is comparing two things and if their bits both equal 1 it returns 1, but I don't know what's going on between those two values, what they resolve to, or what they're supposed to represent. I also don't know what global114 is supposed to be. Any ideas?
(procedure public (proc0_6 param1)
return & global114[(/ param1 16)] (>> $8000 (% param1 16))
)
Two other global procedures utilize this code as well.
Oh, this is a pattern you'll see quite often in SCI1/SCI1.1 - the scripts often use bit flags instead of variables to denote whether a particular event has occurred.
There are three procedures that you will first need to identify - test, set and clear (this one tests the specified flag). Once you've done that, stop focusing on the code in these procedures and instead look at the parameter it's called with. This parameter is a flag number. So in whatever code you're looking at, the flag with that number is being tested - has this event occurred yet, has the player performed a particular action yet, etc. You can try to find the corresponding call that sets the flag (use findstr or whatever tool you have for searching through files; your knowledge of the game itself may also provide clues).
-
I think it might be an efficient way to store important game flags. Normally every variable (even if it's just an on/off flag) takes up 16 bits. This way it only takes up 1.
When you see a / and % operator together, that's usually because someone is trying to split a number into two parts... the quotient and the remainder.
So say param1 is 37. Then we'll get 2 for the quotient (37 / 16), and 5 for the remainder (37 % 16). Then it will return what is in bit 5 of array index 2.
Looks like proc0_7 and proc0_8 are responsible for setting and clearing those bits.
I think did something similar when I made Cascade Quest, so that I could save space for game flags.
My decompiler wasn't able to ascertain any name for the variable.
I do have the ability to detect arrays, but I turned it off for the global script. I can detect arrays based on how the variable is used. But it just scans the current script for variable usage. To see how globals are used I need to scan all the scripts at once. It's probably not too hard to do that.
Just doing a quick string search, it looks like the next variable that is used anywhere after global114 is global128. So that suggests global114->global127 is an array. So ideally the decompiler would produce code like this:
...
gStopGroop
global114[14]
global128
...
And 14 spots of 16 bits each suggests SQ4 handles up to 224 game flags.
[edit... ninja'd by Lars :-)]
The piece of source code from PQ SWAT calls a Bset procedure, which presumably stands for "bit set". So "good" names (to stay true to the original source code) would maybe be Btest, Bset and Bclear.
I attached the SQ4 source code with those changes...
-
Very illuminating! Thank you both. Here's the block of code I'm trying to decipher. I'm trying to figure out how another line is called in a script. Specifically in the center screen on the Xenon streets, when the ship is parked on the ground, interacting with gear1 or flap (the gear flap) will either cause Roger to enter the ship and take off or, if something is tested with proc0_6(), return the message "You're starting to show your age." I'm not sure how to trigger it because I'm not sure what proc0_6() is testing.
This is the doVerb method of the flap Prop. I added some observations and questions as comments:
(method (doVerb param1)
(switch (param1)
(case 2
(if (Btest(0)) // No idea what this is testing. I don't know what the global114 array is supposed to represent, it's only used in the Main script and no where else
(if (not script) // I guess this is checking if there is a script set for the room?
proc0_2() // This removes control from the player
(send global2:setScript(egoHides)) // Animates Roger entering the ship
)(else
Print("You're starting to show your age.")
)
)(else
(super:doVerb(param1)) // Not sure....? It looks like it's retriggering itself. Why won't this enter an infinite loop?
)
)
(default
(super:doVerb(param1)) // Same as above
)
)
)
-
It's testing if flag 0 is set. I did a search for Bset(0), the only hit for that is in rm040.sc. It looks like it's set if the ego came from some room other than north (25), south (55) or east (45).
It looks like my decompiler named the "previous room number" global "gWest", due to an assignment to that variable in rm535.sc. Oh well... can't be perfect.
It doesn't look like any other room *directly* connects to room 40 other than those three. However, searching for "newRoom(40)" shows that this is called in rm072.sc. So presumably going from room 40 via room 72 will set that bit (flag 0).
-
Incidentally, as you're looking through these scripts, I could use feedback on what additional information would be useful for understanding the scripts.
For instance, this
(if (& (send gEgo:onControl(1)) $0080)
would be nicer if it were:
(if (& (send gEgo:onControl(1)) ctlSILVER)
Likewise, in the snippet you posted, "case 2" should really be "case vbLOOK", or whatever verb 2 is (maybe someone could figure out which verbs correspond to which numbers? Is it the same across different games?)
I'm trying to decide whether or not to just have a few hard-coded heuristics for stuff like this, or go for a more flexible system that can read the mapping from some config file.
-
Room 535 is the room where the ship lands inside the landing bay after boarding the ship from the Xenon streets (room 45). You can also take the ship back down to the streets from the landing bay. Room 72 is the shot of Roger peering up from the manhole cover from the sewers as the ship lands. This is the only way to get the ship to land (come from room 72), so I don't understand how coming from that room sets that flag if it's failing the condition and not displaying the message in room 45...
Regarding adding helpful information, that definitely would help quite a lot. We can always make special cases if things end up being different between certain games. It seems like a few of the global variables at least are all the same as the SCI0 template game. That is, global2 is gRoom for instance but it's not named. I noticed that those names are used in your decompiled scripts, but not always and not when they're declared for the most part.
-
Entering room 40 from 72 seems to set the flag. Entering room 72 from somewhere other than 45 or 90 seems to clear it. You should be able to search for Bset(0) and Bclear(0) and try to follow the logic? Can you get to room 45 after going from 40->72, and then not going back to 72?
That is, global2 is gRoom for instance but it's not named. I noticed that those names are used in your decompiled scripts, but not always and not when they're declared for the most part.
The names are given default names depending on how the variables are used, so it's a best guess. The decompiler has functionality to rename things manually and then future decompilations will pick the new namesup.
What do you mean "not when they're declared"?
-
Entering room 40 from 72 seems to set the flag. Entering room 72 from somewhere other than 45 or 90 seems to clear it. You should be able to search for Bset(0) and Bclear(0) and try to follow the logic? Can you get to room 45 after going from 40->72, and then not going back to 72?
No, it's not possible to go back to room 72 from room 40 again or anywhere else except room 90 (sewer room). Strange. It's probably impossible to trigger then. But I still don't understand why the conditional statement was created to begin with. What is the purpose of setting bit flag 0? I'll do some more experimenting.
EDIT: Ahhh...you can't physically go to room 72 from room 45 but when you take the ship back down from room 535 it temporarily switches rooms to room 72 (the manhole cover view) to show the ship coming down and landing and then switches back to room 45 to show Roger get out and run down to room 60 (beginning of the game). That's where the flag is cleared.
The names are given default names depending on how the variables are used, so it's a best guess. The decompiler has functionality to rename things manually and then future decompilations will pick the new namesup.
What do you mean "not when they're declared"?
I apologize, I'm mixing up programming terminology. I probably meant "when they're defined". As in, right at the beginning of the Main script along with all the other globals.
EDIT: I'm sorry! I was mistaken! I just realized I was reading the nested if statements wrong the whole time. It's not the Btest check at all that determines whether the message prints or not (stupid mistake). That is used probably to determine if the ship is visible or not and what the verb actions do (which explains why you can't get back to room 72 from room 40 because the ship must be there until you fly back down from the landing bay, which then flies back when you get out on the streets again). It's rather the:
(if (not script)
proc0_2()
(send global2:setScript(egoHides))
)(else
Print("You're starting to show your age.")
)
...block. It's checking "if (not script)" what is this doing exactly? I can't find where script is defined. I can see it's probably a global thing, but I'm having trouble identifying what its value would be in this scenario. I'm guessing it's checking whether there are any other scripts running in the room or not. I don't know how that would be possible or not in this room, though...
-
script is a property on Prop (flap is a Prop). So if someone did a flap:setScript(someScript), then a script would be attached to flap. I don't see anywhere that does this, but it's possible it's hidden somewhere. Or maybe it was just defensive coding on their part, that if some script was running on flap, they wouldn't want to run the "egoHides" script on the room?
As for the variable naming thing... you aren't seeing anywhere where globals have their "dumb name" (e.g. global21) in some scripts, but the same variable having a "smart name" in another script, are you?
-
No, you know what I was mistaken. I originally thought that global2 was gGame. But failed to realize that gGame was already defined. So I was just remembering when I thought it wasn't. My bad. Everything looks to have one label everywhere. It seems we can match the globals to some of the ones in the SCI0 template game, though. And some others are just obvious by looking at what methods and procedures use them. For variables like global19, global20, and global21 they're obviously cursor presets because one of the values is 999 (a default cursor setting) and the setCursor method uses all of them. The font globals can be similarly deciphered.
-
Yeah, more of them can be deciphered than I've put in heuristics for.
There are some trickier ones too. Based on the kernel tables here: https://github.com/scummvm/scummvm/blob/master/engines/sci/engine/kernel_tables.h, global106 looks like a polyphony flag (DoSound(3)). global105 looks like it is the number of colors the display can handle (Graph(2))... and flag 21 is set based on that, so flag 21 is some kind of display color depth indicator.
And that color depth indicator is used in speedtest.sc, and in proc0_18. And proc0_18 is used in TONS of places to choose between two color values (so it seems). And by looking at the values passed to proc0_18, you can determine that globals 155-161 appear to be colors. Oh, and it turns out they are set in script n802, along with several other global colors.
-
In the debugger in that SCI interpreter which people will not use, try:
vmvars g 114 0000:8000then go north from the starting screen. room is another useful command. If you use it twice in a row with different numbers (saying 'go' in between to allow the room change to take place), the game will think you came from the first room.
-
Incidentally, as you're looking through these scripts, I could use feedback on what additional information would be useful for understanding the scripts.
For instance, this
(if (& (send gEgo:onControl(1)) $0080)
would be nicer if it were:
(if (& (send gEgo:onControl(1)) ctlSILVER)
In this case, it would probably be fine, but what about the instance of $0006 ... bitwise operator stuff where that actually denotes on Navy and Green control colors simultaneously. I don't mean to sound like a stickler here, but as I use the multiple color scenario, I would prefer the value... but I am probably the only one.
-
With a little extra work, I would make that (| ctlNAVY ctlGREEN). But you're saying you prefer just the raw number?
-
I use the raw values a lot... I mean a whole lot, and in each and every script. Almost every interaction with the point and click template makes use of control or priority hot spots. That's why I said it's probably just me who would prefer the raw number. I would wager that almost everyone else would prefer the more readable "name" assigned to it in the sci.sh file.
-
I don't understand, how would assigning the names be better for what you're doing? Are you just used to the numbers is that it?
Maybe just make it an option during compile? Whether to use sci.sh labels or not.
-
I don't understand, how would assigning the names be better for what you're doing? Are you just used to the numbers is that it?
Maybe just make it an option during compile? Whether to use sci.sh labels or not.
Correct I am just used to the numbers... That is all. Well that and when it comes to ego on control statements, as I pointed out earlier, $0006 actually means On Navy and Green... But as I don't plan to do much with decompilations besides learn from them I am good with the colors defined names. No reason even to build in the option. Defined names are fine.
-
I tend to use hex numbers for colors in my code, too.
-
I just never bothered to learn them or I might be agreeing with you.
-
With a small fixed pallet just names might be OK, but a large enough palette ends up with enough variations that you may want an exact color that has no name.
-
Well yeah, but these were only for the priority and control colours weren't they? There's only 16 of them even in SCI1+.
-
Maybe not SCI specifically, but I was talking generally.
-
For code clarity it's probably best to use a define to name the colors specific to what they do in that particular file.
e.g.
(define ctlSpringTrap $0004)
(define ctlDoorOpen $0008)
-
What happens is that these go into an ordered list. If frog is substituted for cables, then it will indeed come before restOfPod. What I was doing was I added it manually by calling frog::init from the ScummVM debugger, in which case frog goes in last. In that case, restOfPod must be removed for things to work.
The reasoning here is wrong, by the way. The entries in the list are prioritized by y coordinate (see the class OnMeAndLowY) if the cursor could be within several rectangles at once. The fix is as stated: Put frog in the list, remove restOfPod.
-
Thanks for that reference Lars, I just spent some time trying to get a click on a feature to take precedence over the click on a view where the two overlapped.
Tried rearranging the order in which it was inited, with no change... and then I remembered that you had posted this.
Threw in a quick y value that was greater (visually lower) than the y property of the view and voila.
-
Probably a good thing to note in the "interactions" tutorial
-
However, apparently in NRS's SQ4 patch he re-enabled the "Time-O-Matic" which is attached to the frog instance. But I can't get it to work...care to take a look at his script with your decompiler?
However, I discovered that his patch does not include the Time-O-Matic time warp after all, it was another script written by someone called the "Time Pod Debug patch" available on the archived SQ.Net website.
My patch (http://www.vogons.org/viewtopic.php?f=7&t=44537) does include it, but it is disabled by default. To enable my additional debug features, create a file named "DEBUG.FLG" in the game's directory, and start a new game.
It's difficult to change the length of a script if you're just directly editing bytecode, so it makes sense that he had to "replace" one object with another.
For that patch, I was not editing bytecode, but disassembling and reassembling with the ability to change script lengths. Remember that the "room of deleted items" (room 271) had no script at all so I had to write one from scratch, and I could not have done that by directly hacking bytecode into a hex editor.
-
Ah, thank you. And welcome! :)