Jump to content

Overhauling the weapon rack scripts


Sclerocephalus

Recommended Posts

Let's look at this from another angle. Will the end user be able to detect the difference in the game between 5 and 6 attempts? No. So is this kind of nitpicking truly necessary?

In my earlier post, I left it alone. Now that it is obvious the loop is failing, it's a good idea to fix all programming errors on the same line at the same time.
Link to comment
Share on other sites

The CoA weapon rack in WarMaiden's has 2 lefts and 2 rights (with 1 mid). Is that correct?

If it is correct, I think the links are wrong. If it is not correct, perhaps we should remove the extras?

EDIT: I'm pretty sure it's wrong. Each side has both a left and a right. But they all have scripts, and that's where some of the extra OnLoads are coming from.

Link to comment
Share on other sites

The CoA weapon rack in WarMaiden's has 2 lefts and 2 rights (with 1 mid). Is that correct?

If it is correct, I think the links are wrong. If it is not correct, perhaps we should remove the extras?

EDIT: I'm pretty sure it's wrong. Each side has both a left and a right. But they all have scripts, and that's where some of the extra OnLoads are coming from.

 

Yes.

 

That's because what you see as one rack are three objects (i.e. three triggers), plus three activators which you don't see. Each trigger is using a different base object, by the way.

 

Have a look at this in the CK, click the activators (the white boxes, they completely cover the plaques) and press "1" twice. This will turn them invisible without deleting them. Now turn on collision (in the drop-down menu under "view preferences") and look at the two knob-shaped objects above the left and the right rack respectively: these are the actual triggers. The shield trigger is visible as a cylinder below the middle plaque.

 

Those triggers are a part of the mesh, but every mesh can only handle one trigger. If you add two trigger volumes, they will still act as one trigger. That is, OnTriggerEnter and OnTriggerLeave events will fire when an object enters either of the volumes, with no way to discern which of the volumes triggered the event (I did run tests with modified trigger meshes for my Extended Racks mod). That's why Bethesda has been using one trigger plus one activator for each weapon to be placed.

 

 

EDIT:

While you're at it, navigate to Hjerim and have a look at the display cases there. They are different from other racks in that they consist of three objects: one visible (the actual display case), and two invisible: a blue one (the trigger), and an orange one (the activator).

 

Finally, you should also have a look at the trigger meshes in nifskope to understand how the racks work. You will notice that there are extra nodes with no mesh information (the names should be familiar from the scripts). The translation and rotation data on these nodes define how an item is placed. When you run the "MoveToNode" command, the engine checks the specified node and reads the data from it, then starts to calculate the offsets to move the item in its position.

Link to comment
Share on other sites

Yes, what?

Of course I'm looking in the CK. So I've figured out the pairing system (some time ago).

The naming of things here is unfortunate, because everywhere else triggers are invisible and activate with the Activate Parent.

Link to comment
Share on other sites

Also thought I'd mention the editor warnings in Warmaiden's -- there are 3 of them.

It says "Please remove shape or convert to primitive." Unfortunately, no object number.

Link to comment
Share on other sites

It says "Please remove shape or convert to primitive." Unfortunately, no object number.

 

Yes, that's the triggers the CK complains about !

Link to comment
Share on other sites

...

 

counter !ItemToPlace.Is3DLoaded() && counter < 5
0              test 1                test 1 true
1              test 2                test 2 true
2              test 3                test 3 true
3              test 4                test 4 true
4              test 5                test 5 true
5              test 6                test 6 false

I'm sorry butting in here and with due respect DayDreamer, but TTBOMK < is less than. 5 isn't < 5:

 

 

 


4              test 5                test 5 false
Link to comment
Share on other sites

Yes, remembering the conditional test is always done at the beginning of a while loop, so the code at the 6th test is never executed.

Link to comment
Share on other sites

I believe he was actually counting the number of times ItemToPlace.Is3DLoaded() was called, not how many times the code inside the while loop executed.  So with the counter check at the end of the expression, the function would get called when the while statement's expression is being evaluated for the 6th time (assuming the 3D of the item is not loaded throughout).

 

Anyway, it's not really important.  Hopefully the greater issue can be quickly resolved, and I (and I know many others) appreciate the work being done by everyone on the weapon racks and the U*P stuff in general.  Thanks.

Link to comment
Share on other sites

Hopefully the greater issue can be quickly resolved.

 

This actually is resolved. Arthmoor has been testing v3.4 already.

 

There was a follow-up problem with Hearthfires, which linked all activators in Honeyside to the player home decoration enable marker. Now, since the activator script interprets any linked item as a pre-placed weapon, this resulted in all scripts placing this x-marker on the racks (the marker got "handed over" from rack to rack, in the order in which the scripts started running) and the racks becoming entirely inoperative. Thus, the links had to go. The immediate result of this was that a Hearthfires script was barking at me, but this problem is resolved now as well.

Link to comment
Share on other sites

I've been going through the creationkit documentation yet again, and can find nothing that says a state is discarded by a new script version or a game reload.

So backwardly compatible, the existing vanilla activators are in state "EmptyRack" at the time the script is first called. Not in state "", as seemed to be assumed.

 

I have finally found the page with some of the comments on savegame/update oddities (the stuff on the form lists at the bottom of the page has been added quite recently; also don't miss the discussion page):

http://www.creationkit.com/Save_File_Notes_%28Papyrus%29#Scripts

 

This still does not make any mention of the states, but I'll find that page too eventually.

 

It's probably worth noting here that the "Patch14CoARacks" bool and the somewhat strange condition checks are an immediate result of those update issues: In the original game, Beth had mistakenly assigned the wrong rack type to some weapon racks, so the player would occasionally end up with weapons floating in the middle of the room when trying to place specific item types on specific racks. They fixed this in patch 1.4, but had to introduce "Patch14CoARacks" as a helper variable because the script refused to accept any changes to the "RackType" property (baked in the save game) unless the player would start a new game.

 

Following up on this, I had to discern two rack types that were not discerned by the vanilla scripts (for placement reasons, i.e. to prevent clipping; this had otherwise nothing to do with the scripts) and was facing the same problem. While I have added the new type (and included separate checks for this new type), this will only take effect on a fresh game. In order to make it also work when the USKP is installed in a running game, I did edit the trigger mesh of that rack. More precisely, I added a new node that has no use except for having its name checked by the script (the infamous "bogus pivot"), so I can discern the racks anyway.

Link to comment
Share on other sites

I'm pretty sure I know now what it is about those certain items (Ebony Blade, Ghostblade, Keening, Wuuthrad, Zephyr, ...) that causes problems. It has nothing to do with being a quest item (as some have mentioned, other quest items work fine, and some non-quest items have problems), or with having persistent references (again, some persistent references work, some non-persistent references don't), or using any particular model or mesh. And ironically, the explanation has been suggested repeatedly in this very thread, but as far as I can tell, never investigated further:

 

The original Ebony Blade has a script attached to it. Could that be a factor?

More precisely, there are two ebony blades, both of which are in possession of the player at some point in the game, and both of them have a script attached. The mere fact that a script is attached should not matter, ...

... A lot of NPCs show up there as their attached script names instead of their expected base actor IDs. So you could be seeing the debug output listing the transient inventory reference instead of the actual form ID for the item.

Any chance the shit hit the fan because it has that disable havok script on it?

I wonder whether it is that stupid script that actually makes Zephyr one of the problem weapons. Any chance that we could sack that script entirely ?

 

It's the script. It doesn't matter what script, or what it does; even an empty script, nothing more than "Scriptname MyItemScript Extends ObjectReference", can cause this issue.

 

It can be difficult to diagnose because scripts can become attached to an object reference in three ways. In the case of Keening, Wuuthrad and the Ebony Blade, the script is attached to the base form, so all instances of these items will have problems, no matter how the player gets them. But scripts can also be attached via cell-placed references, as is the case with Zephyr and Ghostblade. So those items will appear to work fine if you use PlaceAtMe() or AddItem() or somesuch to instantiate a fresh reference from the base form, because that reference has no script, but the instance that the player actually gets in normal game-play comes from a cell-placed reference which attaches the extra script. The only exception is scripts attached via a quest alias, as is the case with Auriel's Bow; scripts attached in this way don't appear to cause problems, possibly because they extend ReferenceAlias instead of ObjectReference, or maybe just because quest aliases are special snowflakes.

 

I have confirmed this explanation by creating a test mod with several copies of the Iron Sword. Each copy is identical to the original form except for its name and scripts. One copy has no script, one copy has a script on the base form, one copy is given a cell-placed reference (in the QASmoke cell) which adds a script, and a fourth copy is created by a quest which attaches a script to the alias. All scripts are empty, they just have the one header line that defines their parent script. The unscripted and alias-scripted copies of these swords work just fine; the form-scripted and reference-scripted copies will misbehave.

 

I've also been experimenting to determine *why* having an attached script is a problem. So far my best guess is that there is some code path in the game engine which causes an object reference to lose its class inheritance data. This in turn causes the engine to start treating that object as an instance of "defaultDisableHavokOnLoad" or "WuuthradScript" or whatever, and *not* as an instance or subclass of ObjectReference. Then the next time Papyrus tries to work with a pointer to this reference, it refuses to call any methods on it which are defined by Form or ObjectReference because doing so means implicitly casting the pointer to Form or ObjectReference, and Papyrus won't do that because it thinks the reference is not a subclass of these scripts. That is what causes the log to show "no native object bound to the script object, or object is of incorrect type" -- I think we have been focusing on the first possibility (no object? it became None? what?), when really it's the second (incorrect type) that is causing the problem.

 

I'm still gathering log data for various events with various kinds of scripted objects, but I wanted to share these initial findings first. I have noticed so far, however, that even when DropObject() and some events start getting broken references that won't cast to ObjectReference, other events seem to continue getting usable reference pointers. So I'm hopeful that maybe we can find a way to make these problematic items start working consistently.

Link to comment
Share on other sites

I'm pretty sure I know now what it is about those certain items (Ebony Blade, Ghostblade, Keening, Wuuthrad, Zephyr, ...) that causes problems. It has nothing to do with being a quest item (as some have mentioned, other quest items work fine, and some non-quest items have problems), or with having persistent references (again, some persistent references work, some non-persistent references don't), or using any particular model or mesh. And ironically, the explanation has been suggested repeatedly in this very thread, but as far as I can tell, never investigated further:

 

 

It's the script. It doesn't matter what script, or what it does; even an empty script, nothing more than "Scriptname MyItemScript Extends ObjectReference", can cause this issue.

 

It can be difficult to diagnose because scripts can become attached to an object reference in three ways. In the case of Keening, Wuuthrad and the Ebony Blade, the script is attached to the base form, so all instances of these items will have problems, no matter how the player gets them. But scripts can also be attached via cell-placed references, as is the case with Zephyr and Ghostblade. So those items will appear to work fine if you use PlaceAtMe() or AddItem() or somesuch to instantiate a fresh reference from the base form, because that reference has no script, but the instance that the player actually gets in normal game-play comes from a cell-placed reference which attaches the extra script. The only exception is scripts attached via a quest alias, as is the case with Auriel's Bow; scripts attached in this way don't appear to cause problems, possibly because they extend ReferenceAlias instead of ObjectReference, or maybe just because quest aliases are special snowflakes.

 

I have confirmed this explanation by creating a test mod with several copies of the Iron Sword. Each copy is identical to the original form except for its name and scripts. One copy has no script, one copy has a script on the base form, one copy is given a cell-placed reference (in the QASmoke cell) which adds a script, and a fourth copy is created by a quest which attaches a script to the alias. All scripts are empty, they just have the one header line that defines their parent script. The unscripted and alias-scripted copies of these swords work just fine; the form-scripted and reference-scripted copies will misbehave.

 

I've also been experimenting to determine *why* having an attached script is a problem. So far my best guess is that there is some code path in the game engine which causes an object reference to lose its class inheritance data. This in turn causes the engine to start treating that object as an instance of "defaultDisableHavokOnLoad" or "WuuthradScript" or whatever, and *not* as an instance or subclass of ObjectReference. Then the next time Papyrus tries to work with a pointer to this reference, it refuses to call any methods on it which are defined by Form or ObjectReference because doing so means implicitly casting the pointer to Form or ObjectReference, and Papyrus won't do that because it thinks the reference is not a subclass of these scripts. That is what causes the log to show "no native object bound to the script object, or object is of incorrect type" -- I think we have been focusing on the first possibility (no object? it became None? what?), when really it's the second (incorrect type) that is causing the problem.

 

I'm still gathering log data for various events with various kinds of scripted objects, but I wanted to share these initial findings first. I have noticed so far, however, that even when DropObject() and some events start getting broken references that won't cast to ObjectReference, other events seem to continue getting usable reference pointers. So I'm hopeful that maybe we can find a way to make these problematic items start working consistently.

 

Wow ...

 

(... a couple of seconds of silence...)

 

:

:

 

A note on Wuuthrad, as there's one particular complication: my copy of Wuuthrad is somewhat unique in that it refuses to stay on a rack every second time, but can be mounted every first time (or vice versa). This is perfectly repeatable, making this weapon the only weapon so far that "survives" the script check. Though, this Wuuthrad is not the original Wuuthrad. The vanilla game "confiscates" the original in Ysgramor's tomb, when the player activates the statue, and returns him a copy when he reclaims it later (the weapon on the statue is yet another copy of Wuuthrad which is enabled or disabled on activation). Now, since any previous tempering gets lost in this operation, the exchange was easily noticed and has always been annoying. Eventually, the USKP fixed this, and what the game returns with USKP installed is the real Wuuthrad. Thus, how the real Wuuthrad behaves could be investigated only very recently, and to my knowledge, it is not a problem weapon - what actually makes sense because I'm pretty sure it's in a quest alias when it enters the game, so to say. I still wonder though where the on-off behaviour of the copy comes from (not that it really matters, just out of interest).

 

Otherwise, I am a little confused, because I always thought that papyrus identifies items with their script name because "script name" is their actual form. If a script is "attached" to a form, it actually extends this form, which means that you get a new form with new properties of the type "script name". Likewise, for example, you can cast the form as "script name" (which is the usual way to access the "attached" script from an external script). Thus, I never wondered much about this convention, since it is only logical. Though, when you say now that it appears that papyrus can't handle the form, does this mean that it is possibly just unable of casting it ? Which, in turn, would mean that they were simply never properly defined (since a base form with a script attached that exists in the master file is not the same as a reference to that form (with the same script attached) which does not exist in masterfile and only appears in the game when it gets generated at runtime by PlaceAtMe() or a similar procedure).

 

Anyway, if you get these critters to work, you will be everybody's hero.

Link to comment
Share on other sites

Heh. Well. You know the saying... go with your gut. Though I don't know what good it would have done, because how do you account for an infinite possibility of scripts that extend ObjectReference?

 

Also, what's causing this to break anyway? It makes no sense since you should be able to treat an object of type "HavokDisabled" as an ObjectReference if HavokDisabled extends it and thus use any function from ObjectReference on it. Unless we just uncovered a hideous engine bug that was unknown before now :P

Link to comment
Share on other sites

Also, what a tragedy with Auriel's Bow: one of the few unique items that would not cuase problems, but Beth found a way to rebalance this by a faulty script ...

Link to comment
Share on other sites

Otherwise, I am a little confused, because I always thought that papyrus identifies items with their script name because "script name" is their actual form. If a script is "attached" to a form, it actually extends this form, which means that you get a new form with new properties of the type "script name". Likewise, for example, you can cast the form as "script name" (which is the usual way to access the "attached" script from an external script). Thus, I never wondered much about this convention, since it is only logical.

 

Well, I think of Forms sort of like the object-oriented programming concept of "classes". It is a base template which has certain properties and behaviors of its own, but you can also instantiate it (create a Reference), and each instance then shares some things with eachother via their common base class, but also has some properties and behaviors separate from any other instance.

 

So at the class (Form) level, yes, attaching a script to a form is like extending a class (and even uses this terminology in the script header), and instantiating this extended class creates an object which can also be treated as an instance of the base class. Normally, Papyrus and the engine handle this just fine; references get cast back and forth seamlessly, which allows you to call Form methods on anything that derives from Form, etc.

 

Of course you can also attach multiple scripts to the same base Form, so it's not as simple as the basic OOP class hierarchy model. I guess you could think of this like the OOP concept of multiple inheritance, but that is not very well defined and is implemented differently in almost every language that has it at all, so the analogy breaks down a bit there.

 

Though, when you say now that it appears that papyrus can't handle the form, does this mean that it is possibly just unable of casting it ?

 

Yes, this is my suspicion. Recall what appears in the log when one of these object references goes bad and you try to call, for example, GetFormID() on it: first, there is an error that "no native object is bound to the script object, or object is of incorrect type". I think this indicates that Papyrus knows it has a pointer to a game engine object, and it knows that you want to call a method on this object which is defined by the Form script, so it tries to cast its pointer to Form. But it fails to do so because it thinks the pointer it has is not a subclass of Form ("object is of incorrect type").

 

After this error comes a second error about "assigning none to a non-object"; I think this means that since Papyrus has failed to call the method (because of the cast error), it has decided to pretend the method returned None and continue on. But since GetFormID() is supposed to return Int, the assembler has allocated an Int-type register to hold the method return value, so it complains about putting None in that Int register, and sets the register to its default value instead (for Int, 0).

 

Which, in turn, would mean that they were simply never properly defined (since a base form with a script attached that exists in the master file is not the same as a reference to that form (with the same script attached) which does not exist in masterfile and only appears in the game when it gets generated at runtime by PlaceAtMe() or a similar procedure).

 

No, because the problem does not appear right away. When you first get a reference to one of these problematic items (for example via OnItemAdded() if the reference is persistent), it can be used normally. Papyrus can call Form- or ObjectReference-defined methods on the reference with no trouble. In your case, it seems sometimes that the weapon rack can handle the object once, or maybe until DropObject() is called on it. But in any case, it is only later that something causes the reference to go bad, and after this time is when Papyrus starts failing to cast it back to Form.

 

One other note: this whole situation also suggests to me that internally, on some level, the Papyrus scripting engine and the main game engine are separate. They exchange data back and forth so that Papyrus can cause changes in the game engine's model of the world, and so the game engine can trigger events in Papyrus, etc, but they are mostly separate engines. This also explains the phrasing of the "no native object bound to the script object" message; Papyrus handles "script objects" for each thing in the game, while the main engine handles the "native object". When the two engines need to talk to each other, one must be bound to the other, but in certain cases this binding breaks. And yet, notice that even when Papyrus' reference to an object stops working because it doesn't think it's a Form anymore, the game engine continues to handle that object normally (physics etc.) So clearly on the game engine side, it still knows to treat the thing as a subclass of all other physical objects in the simulated world, it is only Papyrus that has forgotten that the pointer it has is to an object which is, in fact, a subclass of ObjectReference.

Link to comment
Share on other sites

Also, what's causing this to break anyway? It makes no sense since you should be able to treat an object of type "HavokDisabled" as an ObjectReference if HavokDisabled extends it and thus use any function from ObjectReference on it. Unless we just uncovered a hideous engine bug that was unknown before now :P

 

I fear that this is exactly the case. I'll post all my logs as soon as I finish collecting them so you can see for yourself, but what I see in them so far suggests that, when an item with an attached script goes through a particular engine code path, the engine does something to the native object that causes Papyrus to think its corresponding script object has no parent class, so then it thinks it cannot cast it to that parent class. And it's not just that the engine starts handing broken object reference pointers to Papyrus when it triggers events involving the item -- even reference pointers which were previously received and stored on the Papyrus side, and which worked fine until this point, will suddenly break when the native object goes through this bad code path.

 

Edit: It's also not that Papyrus doesn't think its script object has *any* class, or that its script object breaks entirely. Recall that when my game got stuck in PlaceItem() spewing errors because of this problem (failing to cast the ref to ObjectReference, thus failing to call Is3DLoaded(), thus never breaking the loop), I was able to hotfix it by editing the defaultDisableHavokOnLoad script to define Is3DLoaded() and return True. When I reloaded that save, Papyrus happily called the Is3DLoaded() that I added to the defaultDisable... script, so it clearly still knew that its reference was an instance of that class. It simply forgot that defaultDisable... is a subclass of ObjectReference, so it refused to cast it back to that parent class in order to call the native Is3DLoaded().

Link to comment
Share on other sites

I'm a nearly absolute noob on everything around scripting, but please tell me if what I've just understood is right.

Each problematic weapon/rack association could be treated as the Wuuthrad/Ysgramor's statue one ? I mean, when we try to hang one of these weapons on a rack, should we rather send it in a safe hidden storage chest and physically place a 'fake' one in its intended place on the rack ?

Link to comment
Share on other sites

Each problematic weapon/rack association could be treated as the Wuuthrad/Ysgramor's statue one ? I mean, when we try to hang one of these weapons on a rack, should we rather send it in a safe hidden storage chest and physically place a 'fake' one in its intended place on the rack ?

 

Yes. Except that the safest place is your inventory. Sending these items forth and back between containers will cause them to "break" sooner or later.

 

I already tried a while ago to place dummies on the rack while the true weapon was secured in a storage barrel (placed in an inaccessible holding cell, reference permanently persistent, script applied through a quest alias). It doesn't work! The flaw of this method is the need to use a storage. You can't place a dummy on the rack and leave the true item in the player's inventory! You wouldn't get through with that - everybody would complain; after all, this is like blowing up your own cover. Though, the player's inventory is the only safe place. At some point - usually after only a few trials - the reference got borked, and either transfer into, or even worse, transfer from the storage container would fail.

 

The scripts that come with v3.4 are capable of detecting on their own which item is safe to place and which not. None of the latter will ever be even attempted again at placing on a rack.

Link to comment
Share on other sites

Instead of a storage container, could we just use a dedicated interior (such as the corpses trashcan) and set the fake weapons placed there as persistent references ? When activating a weapon rack, the related scripts would only make a swap before placing. Beware, that may be a foolish idea, I don't know... It's only a noob suggestion.

Link to comment
Share on other sites

Well, removing them from a container (player's inventory, chest or whatever) is the worst thing you can do, as that will break everything immediately. The script check will do exaclty this, but only after it has run another check to have a valid reference as backup. If it turns out that the "world reference" is borked, the true reference provides a last chance to do something with the item, and this is moving it back into the player's inventory. Subsequently, the base object is added to an exclusion list and no reference of it will ever be handled again.

 

EDIT:

Note however, that you still can drop the item anywhere or put it in a container just to your liking, as these actions are not controlled by a payrus script. Only when papyrus takes control, "the shit will hit the fan" as Arthmoor used to say ...

Link to comment
Share on other sites

Instead of a storage container, could we just use a dedicated interior (such as the corpses trashcan) and set the fake weapons placed there as persistent references ? When activating a weapon rack, the related scripts would only make a swap before placing. Beware, that may be a foolish idea, I don't know... It's only a noob suggestion.

 

The danger of moving any item to a player-inaccessible spot, be it a container or cell floor, is that the script must hold on to a (functional!) pointer to the corresponding ObjectReference in order to get it back to the player. Since script pointers to ObjectReferences are known to sometimes become non-functional when the corresponding item has an extra script on it, there's a good chance that before long, the pointer to the item you've stashed away will break and now the player has lost that item forever.

Link to comment
Share on other sites

Silly idea again : could dead and hidden NPCs in the same cell as the racks be considered as chest storages for this purpose ?

Another dumb idea : why not equipping the 'true' weapons on real and hidden mannequins in the same cell ? I mean, do mannequins (which are basically NPCs with absolutely no AI) have the same limitations as chest storages ? After all, placing these weapons in any follower inventory could not cause them to vanish...

Even dumb idea : and placing mannequins dedicated to problematic weapons in an unreachable cell or interior ? So long as the mannequins are 'alive', their references should persist... ?

Link to comment
Share on other sites

Silly idea again : could dead and hidden NPCs in the same cell as the racks be considered as chest storages for this purpose ?

Another dumb idea : why not equipping the 'true' weapons on real and hidden mannequins in the same cell ? I mean, do mannequins (which are basically NPCs with absolutely no AI) have the same limitations as chest storages ? After all, placing these weapons in any follower inventory could not cause them to vanish...

Even dumb idea : and placing mannequins dedicated to problematic weapons in an unreachable cell or interior ? So long as the mannequins are 'alive', their references should persist... ?

 

 

You may do that at any time, but do not let a script handle it.

 

When you have to script the task, you'll inevitably have to handle references. When you do it on your own, you keep this data in your brain. That's irrelevant to the engine.

Link to comment
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
×
×
  • Create New...