ObjectReference Broken Pointer Bug

From the CreationKit Wiki
Revision as of 14:00, 30 October 2013 by imported>Taleden (→‎Testing Methodology)
Jump to navigation Jump to search

Introduction

These are my (taleden's) logs and notes regarding the ObjectReference broken pointer bug, whose symptom is the log error "no native object bound to the script object, or object is of incorrect type". On 2013-10-25 I posted the following to Bethesda's CreationKit forum:

The folks at the Unofficial Skyrim Patch (and probably lots of other modders) have been struggling for months with ObjectReference pointers that go bad, and I think it may actually be a bug in the Papyrus scripting engine, or the game engine, or maybe in the interface between the two.

The symptom of this bug is an error in the debug log that reads "Unable to call [Method] - no native object bound to the script object, or object is of incorrect type". This happens even when the pointer on which the method is being called is not None; when that is the case, the error instead says "Cannot call [Method] on a None object, aborting function call".

I believe the bug arises whenever an object reference has an extra script attached to it (extending the ObjectReference script), and that item is moved from a container/inventory into the world, while it also has a persistent reference.

Here's a short example that illustrates the problem:

Form item = Game.GetForm(0x10AA19) ; Silver Sword
ObjectReference ref = Game.GetPlayer().PlaceAtMe(item)
Debug.Trace(ref+" GetFormID()="+ref.GetFormID()+", SilverPerk="+(ref as SilverSwordScript).SilverPerk)
Game.GetPlayer().AddItem(ref)
Debug.Trace(ref+" GetFormID()="+ref.GetFormID()+", SilverPerk="+(ref as SilverSwordScript).SilverPerk)
ref = Game.GetPlayer().DropObject(item)
Debug.Trace(ref+" GetFormID()="+ref.GetFormID()+", SilverPerk="+(ref as SilverSwordScript).SilverPerk) ; line 33

The log then reads:

[10/25/2013 - 02:10:58PM] [SilverSwordScript < (FF000898)>] GetFormID()=-16775016, SilverPerk=[Perk < (0010D685)>] [10/25/2013 - 02:10:58PM] [SilverSwordScript <Item 3 in container (00000014)>] GetFormID()=-16775016, SilverPerk=[Perk < (0010D685)>] [10/25/2013 - 02:10:58PM] error: Unable to call GetFormID - no native object bound to the script object, or object is of incorrect type stack: [Item 3 in container (00000014)].SilverSwordScript.GetFormID() - "<native>" Line ? [alias PlayerRef on quest TestQuest (0A000D6A)].TestQuest_PlayerRef_Script.OnUpdate() - "TestQuest_PlayerRef_Script.psc" Line 33 [10/25/2013 - 02:10:58PM] warning: Assigning None to a non-object variable named "::temp6" stack: [alias PlayerRef on quest TestQuest (0A000D6A)].TestQuest_PlayerRef_Script.OnUpdate() - "TestQuest_PlayerRef_Script.psc" Line 33 [10/25/2013 - 02:10:58PM] [SilverSwordScript <Item 3 in container (00000014)>] GetFormID()=0, SilverPerk=[Perk < (0010D685)>]

Note that the item must have a script either on its base form (as the Silver Sword does here) or on the particular cell-placed reference that the player picks up (such as Ghostblade and Zephyr), and the reference must be persistent at the moment it is dropped back into the world (as is done here by having the PlaceAtMe()-created reference stored in a running script variable at the time DropObject() is called; PlaceAtMe() can also force the created reference to be persistent, but the result is the same in this test).

When these conditions are met, then any ObjectReference pointer which refers to the item is prone to break, as seen here in the pointer returned from DropObject(). When a pointer breaks in this way, then no native methods can be called on it (such as GetFormID()), however methods which are fully defined in Papyrus on the attached script or any of its parent scripts can still be called normally (such as the implicit SilverPerk property getter function). This implies that the Papyrus script object is no longer correctly linked to its corresponding game engine object, so when it tries to call into the game engine to evaluate a native method, the engine reports that it has no (compatible) object to run it on.

Note that the bug is not limited to DropObject(), that's just the quickest way to invoke it. For example, the player can drop the item manually, and any (persistent) ObjectReference pointer which was previously received and cached in OnItemAdded(), OnItemEquipped() etc. will immediately become broken. Or, the player can drop the item while it is equipped, and then OnItemUnequipped() receives a broken pointer, but OnItemRemoved() receives a functional pointer. The key elements are the persistent object reference, and the extra attached script.

I have a lot more log data if anyone is interested, plus an object reference testing mod that I've been using to diagnose this issue. Apologies also if this has been reported and discussed before, I did a search and only found other players with the same log error, but no investigation or explanation.

This article contains the log data mentioned above.

Testing Methodology

In order to understand these logs, here is the testing methodology that I used to generate them.

I made a testing mod which uses a quest's reference alias on the player to monitor item-related events and analyze the object reference pointers they each receive from the engine. To keep the test cases as clean as possible, the mod also defines six custom items which are all clones of the Iron Sword, with some variation to mimic a particular test case:

  • Test Sword (NP, NS)
  • Test Sword (NP, FS)
  • Test Sword (P, NS)
  • Test Sword (P, FS)
  • Test Sword (P, AS)
  • Test Sword (P, RS)

The tags on each item identify the properties of each test case:

  • "NP" means non-persistent, so these two items are given to the player on game load using AddItem() so that they do not have a persistent reference.
  • The "P" items (except for "P, RS") are granted by the test quest as created reference aliases so that they are persistent.
  • "NS" means non-scripted, so these forms have no extra scripts attached anywhere.
  • "FS" means form-scripted, so these base weapon forms have scripts attached which extend ObjectReference.
  • "AS" means alias-scripted, so the quest reference alias which creates this item also attaches a script which extends ReferenceAlias.
  • The "P, RS" item is placed directly in the qasmoke cell so that it has a persistent world-placed reference, and so that a script can be attached to that placed reference in the CK.

There is no "NP, RS" test case because I don't think it's possible to attach a script via reference without that reference also being persistent (i.e. placed in a cell in the CK). Likewise for "NP, AS", as soon as a quest fills a reference into an alias in order to put an alias script on it, my understanding is that the reference also becomes persistent.

The mod also has a menu control item, and the first time this is activated, the player alias script starts listening for item events (OnItemAdded(), OnItemRemoved(), OnObjectEquipped() and OnObjectUnequipped()). The first time one of these events fires, the base form involved (which should be one of the test items above) is set as the form to be tracked, so that future events are only handled if they involve the same base item. The ObjectReference pointers provided to each event handler are then cached in six different slots, corresponding to six ways they can be acquired:

  • "AW": OnItemAdded() from the world (akSrcContainer == None)
  • "RW": OnItemRemoved() to the world (akDestContainer == None)
  • "AC": OnItemAdded() from a container (akSrcContainer != None)
  • "RC": OnItemRemoved() to a container (akDestContainer != None)
  • "Eq": OnObjectEquipped()
  • "Un": OnObjectUnequipped()

Each cached pointer is filled in the first time each event fires and receives an ObjectReference pointer other than None. On each event, a log message is written that reports the properties of the ObjectReference pointer received by the event: if it is broken (so not even GetFormID() can be called on it without error), the log just shows "(broken)"; otherwise, the log prints the ObjectReference's own FormID, the FormIDs returned by GetBaseObject() and GetParentCell(), and the flags returned by Is3DLoaded() and IsDeleted(). This is followed by the pointer itself cast as a string (which reveals what script Papyrus thinks it represents) and a list of flags indicating whether the pointer compares as equal to any of the previously observed and cached ObjectReference pointers, or whether the received pointer was stored in any of those slots.

Meanwhile, a 10s OnUpdate() loop periodically tests each cached ObjectReference pointer and reports "-" if it is still set to None, "+" if it is set and remains functional (i.e. GetFormID() can be called without the "object is of incorrect type" error), or "!" if it has become broken (GetFormID() appears to return 0, which corresponds with the log error).

Once a form is being tracked, the menu item can also be used to call Player.DropObject(), Player.AddItem(), ref.MoveTo() and ref.Reset(), using the base form or any of the cached ObjectReferences. When calling DropObject(), the return value is also reported and compared to the cached ObjectReference pointers.

Test cases