Jump to content

Photo

Oblivion scripting: reference == 0 fails, why?

Oblivion modding error

  • Please log in to reply
7 replies to this topic

#1
Munch Universe

Munch Universe

    Calamari

    • FOMO - Craftybits
  • 240 posts
  • Location: Baden, Germany

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:

Spoiler

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:

Spoiler

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":

Spoiler

I get the following feedback:

Spoiler

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, 30 August 2016 - 10:36 AM.


#2
AndalayBay

AndalayBay

    #ffc0db Wildebeest under #0000ff Flying Shoes

    • Unofficial Morrowind Patch
    • Dark Brotherhood Chronicles
    • Black Marsh
    • Wild Nuts
    • The Brotherhood of Old
    • TES3Gecko
    • Better Cities
    • Collective
    ▼▲
  • 12,093 posts
  • Location: Ontario, Canada
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.
  • Munch Universe likes this
Madam, you have between your legs an instrument capable of giving pleasure to thousands, and all you can do is scratch it!
-- Attributed to Thomas Beecham in reference to the performance of a female cello soloist

#3
Munch Universe

Munch Universe

    Calamari

    • FOMO - Craftybits
  • 240 posts
  • Location: Baden, Germany

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.



#4
AndalayBay

AndalayBay

    #ffc0db Wildebeest under #0000ff Flying Shoes

    • Unofficial Morrowind Patch
    • Dark Brotherhood Chronicles
    • Black Marsh
    • Wild Nuts
    • The Brotherhood of Old
    • TES3Gecko
    • Better Cities
    • Collective
    ▼▲
  • 12,093 posts
  • Location: Ontario, Canada

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.


Madam, you have between your legs an instrument capable of giving pleasure to thousands, and all you can do is scratch it!
-- Attributed to Thomas Beecham in reference to the performance of a female cello soloist

#5
Munch Universe

Munch Universe

    Calamari

    • FOMO - Craftybits
  • 240 posts
  • Location: Baden, Germany

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:

Spoiler

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:
Spoiler

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:

Spoiler

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):

Spoiler

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, 31 August 2016 - 08:52 AM.


#6
Munch Universe

Munch Universe

    Calamari

    • FOMO - Craftybits
  • 240 posts
  • Location: Baden, Germany

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, 31 August 2016 - 12:49 PM.


#7
AndalayBay

AndalayBay

    #ffc0db Wildebeest under #0000ff Flying Shoes

    • Unofficial Morrowind Patch
    • Dark Brotherhood Chronicles
    • Black Marsh
    • Wild Nuts
    • The Brotherhood of Old
    • TES3Gecko
    • Better Cities
    • Collective
    ▼▲
  • 12,093 posts
  • Location: Ontario, Canada
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.elderscrol...title=PlaceAtMe
Madam, you have between your legs an instrument capable of giving pleasure to thousands, and all you can do is scratch it!
-- Attributed to Thomas Beecham in reference to the performance of a female cello soloist

#8
Munch Universe

Munch Universe

    Calamari

    • FOMO - Craftybits
  • 240 posts
  • Location: Baden, Germany

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.







Also tagged with one or more of these keywords: Oblivion, modding, error

0 user(s) are reading this topic

0 members, 0 guests, 0 anonymous users