Jump to content
  • Announcements

    • AndalayBay

      Leaving IP.Board   11/13/2017

      See full announcement here.
    • AndalayBay

      New Theme Set Up   11/14/2017

      We have a new theme created by Vincent that's now available to everyone. It has been set as the default theme, so everyone should see it when you refresh the page. If you still aren't seeing it, you can select it from the Theme drop-down at the bottom of the page. It's called Assimilation. Thanks Vincent. Awesome looking theme!
Munch Universe

Oblivion scripting: reference == 0 fails, why?

Recommended Posts

I ran into a really odd problem while fixing a bug and thought I would share it. If anyone has any insight into the problem, please share because at face value, my observations make no sense and should be impossible.

 

After updating a script running on a plate used for cooking in CB an unexpected constellation of variable values hits the scripts and causes an error. The error wasn't fatal and I've found a way to fix it in the meantime, so there is no acute problem. The unexpected constellation was caused by a local update that set all variables to 0 while a quest it interacts with still carries a value that indicates a fire has been found, which is no longer true when the local variables were set to 0. One way that fixed the problem was to set the quest value to 0 at the same time that the other variables were set to 0, but before I hit on that, I found the following problem. 

 

Careful tracing of console feedback isolated the problem to this section of code:

 

 

if eval( FoundFire == 0 ) ;
	if eval( CBFindFireQ.rCBFireRef == 0 ); once a fire is found
		set CBFindFireQ.GotFire to 0
		set Reset to 1 ; reset, something went wrong
		DebugPrint "CBCookPlateOS: Error - Invalid fire."
	else ; can only happen if an invalid ref smuggles its way in here
		set rFireRef to CBFindFireQ.rCBFireRef ; load fire into local reference
		DebugPrint "CBCookPlateOS: Fire ref set to %i" rFireRef
		if eval( rFireRef == 0 );
			DebugPrint "CBCookPlateOS: Fire ref is %i, returning" rFireRef
			return
		endif
		DebugPrint "CBCookPlateOS: Fire ref is %i, non-zero, check disabled" rFireRef
		if eval(( rFireRef.GetDisabled == 0 )) ; ( GetDistance rFireRef < 75 ) && 
			DebugPrint "CBCookPlateOS: Got current fire %i" rFireRef
			set FoundFire to 1 ; switch on cooking
			set StopSearch to 1
		else ; fire too far away or disabled
			;if eval( GetDistance rFireRef > 300 ) ;leave some leeway because GetDistance is too quick to trigger a new search
				set rFireRef to 0 ; initialize local fire ref
				set CBFindFireQ.GotFire to 0 ; search for another fire
				set StopSearch to 0
				; set NearFire to 0 ; attempted fix for when the fire goes out due to lack of fuel
				DebugPrint "CBCookPlateOS: Fire disabled or too far away"
				if eval( GetDisabled == 1 ); Fallback delete routine in case fire seems continually too far away
					set DelPlate to 1
					DebugPrint "CBCookPlateOS: DelPlate set to 1"
				endif
			;endif
		endif
	endif

 

 

Note the line in commented with ";once a fire is found", second from the top.

 

This is the console feedback from a run which involves activating the plate near a fire:

 

 

CBKey begin

CBKey: convert in place

CBCookPlateOS: Complete reset by auto-update

CBCookPlateOS: Fire ref set to 00000000

CBCookPlateOS: Fire ref is 00000000, non-zero, check disabled

Error in script 09002737

Attempting to call a function on a NULL reference or base object

    File: CLS-Craftybits.esm Offset: 0x0001 Command: <unknown>

Error in script 09002737

An expression failed to evaluate to a valid result

    File: CLS-Craftybits.esm Offset: 0x0001 Command: <unknown>

CBCookPlateOS: Fire disabled or too far away

CBCookPlateOS: Tell CBFindFireQ to search for fire

qqq

Bye.

 

 

So if the line mentioned above is doing what it is supposed to and detecting that CBFindFireQ.rCBFireRef is 0, the three lines immediately below it should run. But instead it is clear that the else clause is running instead. The only conclusion I can draw from this is that  if eval( CBFindFireQ.rCBFireRef == 0 ) is failing for some reason.

 

Now with a single change to the line of code commented with "; once a fire is found":

 

 

if eval( FoundFire == 0 ) ;
	if eval( CBFindFireQ.rCBFireRef IsFormValid == 0 ); once a fire is found
		set CBFindFireQ.GotFire to 0
		set Reset to 1 ; reset, something went wrong
		DebugPrint "CBCookPlateOS: Error - Invalid fire."
	else ; can only happen if an invalid ref smuggles its way in here
		set rFireRef to CBFindFireQ.rCBFireRef ; load fire into local reference
		DebugPrint "CBCookPlateOS: Fire ref set to %i" rFireRef
		if eval( rFireRef == 0 );
			DebugPrint "CBCookPlateOS: Fire ref is %i, returning" rFireRef
			return
		endif
		DebugPrint "CBCookPlateOS: Fire ref is %i, non-zero, check disabled" rFireRef
		if eval(( rFireRef.GetDisabled == 0 )) ; ( GetDistance rFireRef < 75 ) && 
			DebugPrint "CBCookPlateOS: Got current fire %i" rFireRef
			set FoundFire to 1 ; switch on cooking
			set StopSearch to 1
		else ; fire too far away or disabled
			;if eval( GetDistance rFireRef > 300 ) ;leave some leeway because GetDistance is too quick to trigger a new search
				set rFireRef to 0 ; initialize local fire ref
				set CBFindFireQ.GotFire to 0 ; search for another fire
				set StopSearch to 0
				; set NearFire to 0 ; attempted fix for when the fire goes out due to lack of fuel
				DebugPrint "CBCookPlateOS: Fire disabled or too far away"
				if eval( GetDisabled == 1 ); Fallback delete routine in case fire seems continually too far away
					set DelPlate to 1
					DebugPrint "CBCookPlateOS: DelPlate set to 1"
				endif
			;endif
		endif
	endif

 

 

I get the following feedback:

 

 

CBKey begin

CBKey: convert in place

CBCookPlateOS: Complete reset by auto-update

CBCookPlateOS: Error - Invalid fire.

CBCookPlateOS: Roasting plate reset. Previous cooking canceled.

CBCookPlateOS: fEndTime initialized in Reset 1

CBCookPlateOS: Deleted unneeded references

CBCookPlateOS: Tell CBFindFireQ to search for fire

CBFindFireQ: The search begins

CBFindFireQ: Checked fire ref 0A00B426

CBFindFireQ: Checked fire ref 74003E9C

CBFindFireQ: Checked fire ref 57003904

CBFindFireQ: Checked fire ref 0002A2D4

CBFindFireQ: Checked fire ref 0002A2D5

CBFindFireQ: Checked fire ref 0002A2D7

CBFindFireQ: Checked fire ref 1A006EDC

CBFindFireQ: Fire acquired 1A006EDC on pass 2

CBCookPlateOS: Error - Invalid fire.

CBCookPlateOS: Roasting plate reset. Previous cooking canceled.

CBCookPlateOS: fEndTime initialized in Reset 1

CBCookPlateOS: Deleted unneeded references

qqq

Bye.

 

 

This shows that IsFormValid == 0 is getting the job done. "The Error - Invalid fire" line is running and the script auto-corrects the problem. 

 

The one significant difference between Reference == 0 and Reference IsFormValid == 0 is that the latter will treat a disabled Reference as 0, while a disabled Reference will often still have a FormID which should be picked up if the value is non-zero. But for some unknown reason, the check is failing. In this particular case, IsFormValid works fine for the situation, so I have a solid fix for this problem. My concern is more of a theoretical nature, since I can't explain how Reference == 0 fails and I use it deliberately in other cases.

 

My question for the local experts is how can Reference == 0 be failing when the console feedback reports the value of the Reference to be 0000 0000?

 

EDIT: It occurred to me that the problem could, conceivably, relate to the use of eval. I ran an explicit test just like the first one, except the key line read:

if ( CBFindFireQ.rCBFireRef == 0 ); once a fire is found

 

The results were identical with those from the test where eval is present. That rules out eval as the source of the problem and thus pushes it into something in the scripting language or a lower level.

Edited by Munch Universe

Share this post


Link to post
Share on other sites

Null is not zero. :) Oblivion doesn't have a test for null, unfortunately, but when testing references, you can't replace null with zero. Try not equal to 1 instead. Or use an OBSE function to test for null. I'd have to look at my code to see how I handled those situations. I believe I used not equal to 1.

 

Null is undefined. It isn't equal to anything, so it requires special handling.

Share this post


Link to post
Share on other sites

Actually I started with != 0 and worked from there. I figured it would be easier to understand the story with 0 and inverted clauses, but the results were the same. To the best of my knowledge, using 'eval' invokes obse to do the test. Is there another way to get an answer from obse instead of the built in code? In any case, as I mentioned in the edit, it doesn't matter if I use eval or not.

 

All the same, I'm sure you are on to something. I could imagine that it has something to do with inadequate handling of memory slots. If a memory slot is 64 bit but only 32 are used, it seems conceivable only the 32 are written, leaving the rest to contain whatever they contain. If they are non-zero and the memory call fetches all 64 bits, something like this could happen. Worse, it wouldn't happen all the time, but only when certain memory slots are used. Though in my hands, the behavior is 100% repeatable, beginning from the same save at least. It isn't easy to test from a completely new situation.

 

I can only guess that the obse troop figured out a way to deal with this problem and built it into IsFormValid. QQuix taught me about the existence of this function, which is really useful but is poorly documented. Once you know what it does, the name make sense, but if you just read the name it doesn't immediately suggest where you might use it. I find it a life saver in protecting functions like GetDistance from running on a null reference and throwing an error.

 

Do you know of something like IsFormValid, except that it only catches the null references and allows the disabled references to pass through? There are occasions, like here, where I'd like to run a separate test for disabled.

Share this post


Link to post
Share on other sites

There are various things you can do. You start with IsFormValid. If that's true, then you can use functions like GetFormIDString. Another option is to just use

 

if ( reference )

 

Don't equate it to anything. If that's true, it's not null. I have had to handle nulls, but I can't find the scripts right now. I will say that I think it's better to assign the reference to a variable and test that rather than trying to do everything with an eval. I'll keep looking for the scripts but it will be a while given that I'm not modding right now.

Share this post


Link to post
Share on other sites

Hi AndalayBay,

thanks very much for the responses. Doesn't look like if (reference ) works any differently than the other iterations. I tried this code:

 

				if eval( FoundFire == 0 ) ;
					if CBFindFireQ.rCBFireRef; once a fire is found
						set rFireRef to CBFindFireQ.rCBFireRef ; load fire into local reference
						DebugPrint "CBCookPlateOS: Fire ref is %i" rFireRef
						if eval(( GetDistance rFireRef < 75 ) && ( rFireRef.GetDisabled == 0 ))
							DebugPrint "CBCookPlateOS: Got current fire %i" rFireRef
							set FoundFire to 1 ; switch on cooking
							set StopSearch to 1
						else ; fire too far away or disabled
							if eval( GetDistance rFireRef > 300 ) ;leave some leeway because GetDistance is too quick to trigger a new search
								set rFireRef to 0 ; initialize local fire ref
								set CBFindFireQ.GotFire to 0 ; search for another fire
								set StopSearch to 0
								; set NearFire to 0 ; attempted fix for when the fire goes out due to lack of fuel
								DebugPrint "CBCookPlateOS: Fire disabled or too far away"
								if eval( GetDisabled == 1 ); Fallback delete routine in case fire seems continually too far away
									set DelPlate to 1
									DebugPrint "CBCookPlateOS: DelPlate set to 1"
								endif
							endif
						endif
					else ; can only happen if an invalid ref smuggles its way in here
						set Reset to 1 ; reset, something went wrong
						DebugPrint "CBCookPlateOS: Error - Invalid fire."
					endif

 

 

and got this as the console output:

CBKey begin
CBKey: convert in place
CBCookPlateOS: Complete reset by auto-update
CBCookPlateOS: Fire ref is 00000000
Error in script 09002737
Command GetDistance failed to execute
    File: CLS-Craftybits.esm Offset: 0x0001 Command: <unknown>
Error in script 09002737
An expression failed to evaluate to a valid result
    File: CLS-Craftybits.esm Offset: 0x0001 Command: <unknown>
 
The fourth line of the console output comes from the fourth line of the code block, so even though the fire ref is 00000000 when output, the code block is running and the if (reference ) fails to prevent it.
 
In the meantime I found a second case where it is even more important, fortunately also a solution:

	if eval( DelUnused == 1 )
		if rTargetDel
			if rTargetDel.GetDisabled == 1
				set DelUnused to 0
				rTargetDel.DeleteReference
			endif
		endif
	else

 

 

What this little block of code does is to ensure an item which is no longer needed in the game is permanently deleted with DeleteReference. One important criteria of DeleteReference is that the object must be disabled at the time and disabling takes a frame, so the code must be built to handle this over two or more cycles. The way I implemented this was to use DelUnusued to signal that rTargetDel is ready to be deleted. rTargetDel gets the value of the object to be deleted. In this case, rTargetDel must have a non-zero value and must be disabled, thus I can't use IsFormValid for the first pass, since a disabled object fails this test.

 

I found that when I use this:

 

	if eval( DelUnused == 1 )
		if IsReference rTargetDel
			if rTargetDel.GetDisabled == 1
				set DelUnused to 0
				rTargetDel.DeleteReference
			endif
		endif
	else

 

 

this gets the job done. It appears that IsReference will ensure that a particular reference is not a base object, which is the obvious use for the function, but also returns as false if IsReference is null, which is exactly what I'm looking for.

 

Either this is the source of low frequency bugs within CB and has been for a long time or something changed. I could swear this used to work just fine, though I don't recall ever testing this explicitly. It leaves me wondering if it might be a bug in the CSE, where it is compiling something different than it used to.

 

Using IsReference like this (see the third line, the only difference):

 

 

				if eval( FoundFire == 0 ) ;
					set rFireRef to CBFindFireQ.rCBFireRef ; load fire into local reference
					if eval( IsReference rFireRef == 1 ); once a fire is found
						DebugPrint "CBCookPlateOS: Fire ref is %i" rFireRef
						if eval(( GetDistance rFireRef < 75 ) && ( rFireRef.GetDisabled == 0 ))
							DebugPrint "CBCookPlateOS: Got current fire %i" rFireRef
							set FoundFire to 1 ; switch on cooking
							set StopSearch to 1
						else ; fire too far away or disabled
							if eval( GetDistance rFireRef > 300 ) ;leave some leeway because GetDistance is too quick to trigger a new search
								set rFireRef to 0 ; initialize local fire ref
								set CBFindFireQ.GotFire to 0 ; search for another fire
								set StopSearch to 0
								; set NearFire to 0 ; attempted fix for when the fire goes out due to lack of fuel
								DebugPrint "CBCookPlateOS: Fire disabled or too far away"
								if eval( GetDisabled == 1 ); Fallback delete routine in case fire seems continually too far away
									set DelPlate to 1
									DebugPrint "CBCookPlateOS: DelPlate set to 1"
								endif
							endif
						endif
					else ; can only happen if an invalid ref smuggles its way in here
						set Reset to 1 ; reset, something went wrong
						DebugPrint "CBCookPlateOS: Error - Invalid fire."
					endif

 

 

generates this console output:

 

CBKey begin
CBKey: convert in place
CBCookPlateOS: Complete reset by auto-update
CBCookPlateOS: Error - Invalid fire.
 

Here the fourth line shows that IsReference is returning false, thus the GetDistance functions aren't running and the script doesn't throw errors.

 

Your suggestion to use GetFormIDString is a creative idea, but I'm worried that it is slow or requires more memory. All we need is a quick binary decision but creating a string and comparing each digit seems like it will take a lot more CPU time. I realize that with modern computers speed is hardly an issue in most cases and I've never noticed CB to be the source of slow downs, but on principle I'd only use a string comparison as a last resort if nothing else worked.

 

So far I've never detected a difference between how a properly formed eval functions compared to not using eval, but I have found instances where the compiler doesn't like eval but will accept a bare if. For example this:

if eval(( rTrigRef == r3 + rTrigRef == r2 + rTrigRef == r1 ) == 0 ) 

is rejected by the compiler in the CSE while this:

if ( rTrigRef == r3 + rTrigRef == r2 + rTrigRef == r1 ) == 0

is accepted. But by reformulating the line like this:

if eval(( rTrigRef == r3 || rTrigRef == r2 || rTrigRef == r1 ) == 0 )

the compiler does accept it. I find this to be desirable behavior, since the || conditions are easier to read than the +, even though the test is actually the same. As I see it, using eval systematically puts the statement through a slightly more vigorous test before it compiles, which is likely to eliminate some subtle errors, though it also makes some legitimate code impossible. But since there is always an alternative to the legitimate code which is usually easier to read, I don't see a disadvantage in using eval all the time.

 

Thanks again for sharing your experience. There aren't so many people around who really understand the nuts and bolts of scripting Oblivion. I thought this might be worth discussing since it really is counter-intuitive behavior that is likely to trip up even experienced scripters, much less beginners.

Edited by Munch Universe

Share this post


Link to post
Share on other sites

The story gets weirder still. 

if eval( rReference ) 

appears to work sometimes and not in others.

					if eval( rProduct )
						DebugPrint "CBClayBoardPotteryOS: Product selected %n" rProduct
						let rNewItem := PlaceAtMe rProduct
						set PlaceNewItem to 1
					else
						DebugPrint "CBClayBoardPotteryOS: Zero, Product selected %i" rProduct
					endif

Using this bit of code in another script, the first line is behaving as it should. If product is set, I see Product selected name and otherwise Zero, Product selected 00000000

 

Here is a snippet from the console:

CBClayBoardPotteryOS: Reset
CBClayBoardPotteryOS: Clay, medium loaded as r3
CBClayBoardPotteryOS: Zero, Product selected 00000000
CBClayBoardPotteryOS: Weight change reset
CBClayBoardPotteryOS: Reset
CBClayBoardPotteryOS: Knife loaded.
CBStoneChipQS: No valid target.
CBClayBoardPotteryOS: Clay, medium loaded as r3
CBClayBoardPotteryOS: Product selected Tan Bowl
CBClayBoardPotteryOS: DelUnused set
CBClayBoardPotteryOS: Placed new product Tan Bowl
CBClayBoardPotteryOS: DelUnused completed
CBClayBoardPotteryOS: Reset
CBClayBoardPotteryOS: Tan Bowl loaded as r3
CBClayBoardPotteryOS: Zero, Product selected 00000000
 
So before rProduct has had any value assigned to it, it qualifies as false, it qualifies as true for the one cycle where the product is assigned and distributed, then reverts to 0 as a result of the Reset. 
 
The one thing that may be different between the two is that this testing is occurring with an early save, while the testing mentioned earlier uses much later saves that have gone through more than one generation of that particular script. As far as I know, variables are stored in order in the savegame, essentially anonymously, based on sequence alone. Maybe there is some kind of mechanism to protect a mod from variable reordering which gives the variable a non-null, non-valid value? I'm going to run some more tests to see if I can figure out what exactly is going on.
 
EDIT: if eval( rProduct ) and if ( rProduct ) behave identically. I thought it was important to rule out the possibility that they might be behaving differently early.
Edited by Munch Universe

Share this post


Link to post
Share on other sites

I've got more comments on all this, but I need to grab some code from my game machine and haven't had a chance to do that today. I disable objects and delete references all the time in IFR.

 

I will say that in terms of bugs, it's the other way around. There are a lot of buggy scripts that would compile with the original CS that will not compile in the CSE. The CSE fixes the bugs and has better syntax checking.

 

In terms of references and PlaceAtMe, have you read the comments on this page? http://cs.elderscrolls.com/index.php?title=PlaceAtMe

Share this post


Link to post
Share on other sites

I can't find a way to test the behavior of if rReference that doesn't rely on a savegame in the same series at the moment. A test with an early savegame doesn't reveal the if rReference problem, but at that point all the variables are "fresh" and exactly what the script expects them to be. So I think it is highly likely this problem is a side effect of script changes during a savegame series. The good news is that it is unlikely to apply to others, unless the one-time upgrade is enough to trigger the problem. On the other hand, there is no good reason not to code in a manner that is robust enough to deal with such an upgrade anomaly.

 

I concur about the CSE being much better at catching bugs. The trick adding references that the CS accepts but the CSE doesn't is really unnecessary. I suspect 99% of the time such a construct is a mistake, so banning the addition of references is a good idea. It was simply that one of my predecessors hit on that idea as an easy way to test a number of references at the same time to see if any of them is non-zero.

 

I'm not sure I have read all of the PlaceAtMe entry before, but I didn't spot anything that came as a surprise. Since Craftybits creates hundreds of items with PlaceAtMe, the problem is extreme in our case. Someone came along several years ago and demonstrated the severity of the problem, so we added a mechanism to delete references whenever possible. An old mechanism was already in place, but it was not done properly so it ended up sending all the extra junk to the CBWarehouse. Eventually a cell reset would probably take care of the problem, but it wasn't reliable and the savegame bloat was easily measurable. In the meantime I have a mechanism in place which disables objects at the point where they are no longer necessary, assigns them to a reference only used to eliminate them and sets a flag. In the next cycle the flag allows access to the block which checks for disable then uses DeleteReference on the object. If the object isn't yet disabled for whatever reason, the flag remains up and can try again in following cycles until it is successful. But this means that zero references can't be allowed to sneak into this block, since the GetDisabled check will throw an error. It looks like IsReference does the job.

 

I'll be away for a few days and there is no reason to hurry this discussion. When you find time, however, I'd be interested in seeing what mechanism you use to disable and delete references. It is pretty hard to test, so I don't really have proof that my current mechanism is the best one. I appreciate your comments.

Share this post


Link to post
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

×