スクリプトによるCTDを回避する

Modを作ろう

CTDとはCrash To Desktopの略で、いわゆる不正終了です。ゲームをプレイ中に突然デスクトップ画面に戻ることからCTDと言われます。Skeletonが正しくない(メッシュの要求を満たしていない)とか、原因はさまざまですが、スクリプトが原因のCTDをまとめてみます。

IsHostileToActor

IsHostileToActor

引数がNoneだとCTD、ということで呼び出す前に検査します。

IsHostileToActor (papyrus)

if akOtherActorRef
    if akTarget.IsHostileToActor(akOtherActorRef)
        ; 敵対しているときの処理
    endif
endif

これはやってしまいがちと思います。

いつ初期化されるかわからない場合はこちら。

IsHostileToActor その2 (papyrus)

Actor MyVictim ; 攻撃対象のActor、スクリプト内で共有

Function MyFunction(Actor akTarget)
    Actor akOtherActorRef = MyVictim ; ローカル変数に入れてしまう

    if akOtherActorRef ; Noneではないことを確認する
        ; この時点でMyVictimはNoneになっているかもしれない
        ; でもakOtherActorRefはNoneにならないので安全
        if akTarget.IsHostileToActor(akOtherActorRef)
            ; 敵対しているときの処理
        endif
    endif
EndFunction

このようにプレイヤーをGetPlayer関数で取るのはやめた方がいいと思います。

GetPlayerでCTDする例 (papyrus)

Scriptname SampleScript extends ActiveMagicEffect

Actor MySelf
Actor PlayerRef

; 魔法が掛かったときに呼ばれる
Event OnEffectStart(Actor akTarget, Actor akCaster)
    MySelf = akTarget
    PlayerRef = Game.GetPlayer() ; PlayerRefを埋める
EndEvent

; 死んだときに呼ばれる
Event OnDeath(Actor akKiller)
    if MySelf.IsHostileToActor(PlayerRef) ; PlayerRefがNoneだったらここでCTD!
        ; 敵対NPCが死んだときにここで何かする
    endif

    Dispel()
EndEvent

対策はCKでPlayerRefを紐づけることです。これならスクリプトが稼働した時点で既にPlayerRefが埋まっています。あるいはステートを使い、PlayerRefが埋まる前にOnDeathイベントを処理しないようにする方法もあります。

PlaceActorAtMe

PlaceActorAtMe

ActorBaseを元にActorを生成する関数ですが、特定状況下でCTDします。

  • 引数に指定したActorBaseがテンポラリ(ロード順が0xFF)である
  • そのActorBaseがGCで回収済みである(整理されていて、もうゲームエンジン内に存在しない)

PlaceActorAtMe (papyrus)

ActorBase akActorBase = akTarget.GetLeveledActorBase()

if Math.LogicalAnd(akActorBase.GetFormID(), 0xFF000000) == 0xFF000000
    ; このActorBaseはテンポラリなので使うべきではない
else
    Actor kNewActor = Game.GetPlayer().PlaceActorAtMe(akActorBase)
endif

Actorを動的生成する場合は要注意です。

RemoveAllItems

RemoveAllItems

これは経験則なのですが、一定条件化でCTDすることがあるようです。

  • Actorから移動、もしくはActorへ移動
  • Actorの3Dがロードされている(Playerと同一Cellにいる)

確定でCTDするのではなく、ランダムでCTDします。対策としては以下があります。

  • RemoveAllItemsを使うのではなく、RemoveItemでひとつずつ処理する
  • Actorを別のCellに移動させてから処理する

推測になりますが、RemoveAllItemsはゲームエンジンレベルにてコンテナ内のすべてのアイテムが取り除かれるため、OnItemAdded/OnItemRemovedイベントが一斉に発火するため、そのイベントを見ているスクリプトが多いほどCTD率が上がるのではないかと思われます。Fallout 4でも同じ問題があり、スクリプトエンジンが処理できるしきい値を超えると確定でフリーズするようです。

TapKey

短時間に連続で実行すると確定でCTDします。おそらくは何を押すかでかわってきます。

スクリプトかPOVを切り替えるのはGame.ForceThirdPersonもしくはGame.ForceFirstPersonでいいのですが、Immersive First Person View環境下だとこれでは効かないため、POVキーを取得してTapKeyを使って押すことで変更できます。このPOV切り替えが連続するとCTDします。

GetLinkedRef

GetLinkedRef関数で得られるはずのObjectReferenceが存在しなくなっているとCTDするようです。

GetLinkedRef関数はバニラで、SetLinkedRef関数はPO3 Papyrus Extenderにあります。

バニラにおいてLinkedRefを設定するのはプラグインのみであり、ゲーム内で動的に設定する手段はないようです。つまり、LinkedRefで設定するObjectReferenceは、明示的にスクリプトでDeleteした場合を除き、基本的にはゲーム内に存在しています。

一方でSetLinkedRef関数は任意のObjectReferenceを指定でき、動的に生成されたObjectReferenceを指定した場合、永続であることが保証されないため、いつのまにか消えていることがあります。あるいは明示的にDeleteした場合は即座に消えるため、それ以降のGetLinkedRef関数はCTDしてしまいます。

Scriptname MyAwesomeQuestScript Extends Quest

; CKで紐づけしておく
ObjectReference Property MyAwesomeMarkerRef Auto
Keyword Property MyAwesomeKeyword Auto
Form Property XMarker Auto

; CTDする関数
Function Test()
    ; マーカーを動的に生成する
    ObjectReference kMarkerRef = MyAwesomeMarkerRef.PlaceAtMe(XMarker)

    ; LinkedRefを設定
    PO3_SKSEFunctions.SetLinkedRef(MyAwesomeMarkerRef, kMarkerRef, MyAwesomeKeyword)

    ; 動的に生成したマーカーを削除
    kMarkerRef.Delete()
    kMarkerRef = none

    ; LinkedRefを参照(CTD)
    ObjectReference kLinkedRef = MyAwesomeMarkerRef.GetLinkedRef(MyAwesomeKeyword)
    Debug.Trace("My LinkedRef is " + kLinkedRef)
EndFunction

対策ですが、かなり難しいと思います。

  • SetLinkedRefで設定するObjectReferenceは絶対に消さない。
  • SetLinkedRefで設定するObjectReferenceが消えている可能性がある場合は、GetLinkedRefを実行しない。
  • SetLinkedRefで設定するObjectReferenceが消える前に、SetLinkedRefでnoneを設定してLinkedRefを消す。

例えばCellがDetachされると、その後のCellのLoadで消される可能性があるため、Detachの時点でLinkedRefを消しておくと、かなり安全になります。

Scriptname MyAwesomeMarkerScript Extends ObjectReference

Keyword Property MyAwesomeKeyword Auto

Event OnCellDetach()
    PO3_SKSEFunctions.SetLinkedRef(Self, none, MyAwesomeKeyword)
EndEvent

ObjectReferenceはReferenceAliasに属している間は消えないようなので、そうするのも手です。

タイトルとURLをコピーしました