Jump to content

[LE] Oddity with states


foamyesque

Recommended Posts

This is something of a weird corner case I've encountered in the course of building a fairly complicated script that employs multiple states.

 

The intended state sequences, when the game is first loaded, are one of the following:

 

Empty -> Preboot -> Initializing -> Restocking -> Active [the script's active and ready to go]

Empty -> Preboot -> Initializing -> Inactive [the script is currently inactive, but set up correctly and can be turned on later]

Empty -> Prebbot -> Initializing -> Error [an unrecoverable error was hit during initialization, and the script is now disabled]

 

These flows work correctly on starting a new game, loading an old game if it's the first in session, or loading an old game if it's *not* the first in session but does not have a lot of scripting loading at the same time.

 

However, it breaks in the event of loading an old game with a lot of script nonsense (I have a pretty dirty save I am using to test this) if that game is *not* the first one loaded in the session (works correctly if it is).

 

In that case, all the state flows execute normally, according to traces in OnBeginState, until their terminal state, but then somehow seem wind up back in the empty state, and without triggering an OnEndState event for the state left or an OnBeginState for the empty state. Only functions and events defined in the empty state execute, not anything in what the traces claim was the last state the script was in.

 

It's particularly confusing as the precise same code executes properly otherwise and appears to follow the exact same code path as far as the traces go until suddenly it doesn't. I've audited my code looking for bad GotoState statements and have not found any.

 

Does anyone have any idea why this might be occurring and/or some way to prevent it?

Link to comment
Share on other sites

Maybe the next sample will be helpful.

 

sample script

 

Scriptname XYZ_Test extends ???

; https://forums.nexusmods.com/index.php?/topic/6003658-oddity-with-states/
; foamyesque wrote: "It is particularly confusing as the precise same code executes properly otherwise
;     and appears to follow the exact same code path as far as the traces go until suddenly it does not."

; There are not so many vanilla scripts which are using both events in the same state.
; I found these "NorRotatingDoorLeverScript.psc", "DLC1VQ01PuzzleBrazierScript.psc" for example.


; -- EVENTs --  make sure you do not have used "auto state" anywhere

EVENT OnInit()                                            ; this is empty state
    gotoState(" Initializing")        ; ### STATE ###
    RegisterForSingleUpdateGameTime(0.0)
ENDEVENT


;======================
state Initializing
;=================
EVENT OnBeginState()
    Debug.Trace(self+" [Initializing] OnBeginState() - has been reached..")
    ; do never use any gotoState() here !!!
    ; OnInit() event (the caller) is still running, script will be locked and no other event can be triggered until this event will be closed
ENDEVENT

EVENT OnUpdateGameTime()
    ; initialization code right here, OnInit() event is closed (fastest way) and does not lock the papyrus script anymore

;IF ()
;    gototstate("Inactive")        ; ### STATE ###
;    RegisterForSingleUpdate(1.0)
;    RETURN    ; - STOP -    condition made script inactive
;ENDIF
;---------------------
;IF ()
;    gototstate("Done")        ; ### STATE ###
;    RETURN    ; - STOP -    condition found error occured
;ENDIF
;---------------------
    ; otherwise go to "active" state
    gototstate("Active")        ; ### STATE ###
    RegisterForSingleUpdate(1.0)
ENDEVENT

EVENT OnEndState()
    Debug.Trace(self+" [Initializing] OnEndState() - has been reached..")
    ; do never use any gotoState() here !!!
ENDEVENT
;=======
endState


;======================
state Active
;===========
EVENT OnLoad()        ; this event is not working for Quest scripts, works for ObjectReference you know
ENDEVENT

EVENT OnUpdate()
    ; your active code here
;IF ()
    RegisterForSingleUpdate(1.0)        ; do update chain depends on condition if you like
;ENDIF
ENDEVENT
;=======
endState


;======================
state Inactive
;=============
;EVENT OnLoad()        ; you do not need to cover the same event in state "Active", if not used here
;ENDEVENT

EVENT OnUpdate()
    ; your inactive code here or let this event empty
    RegisterForSingleUpdate(1.0)        ; do update chain
ENDEVENT
;=======
endState


;======================
state Done    ; normally do nothing here
;=========
EVENT OnBeginState()
    Debug.Trace(self+" [Done] OnBeginState() - has been reached..")
ENDEVENT
;=======
endState

 

 

Edited by ReDragon2013
Link to comment
Share on other sites

ReDragon: Something about executing the shifts in state *in conjunction* with a heavy script load *and* not loading that save game as the first in session produces a result that should, according to the documentation, be impossible.

 

This is the state flow, working as intended, in a game started via coc whiterunorigin:

[09/19/2017 - 03:31:34PM] SM: StoreMannequinActor: SM_WR_Warmaidens_Actor01F: OnEndState: State: EMPTY
[09/19/2017 - 03:31:34PM] SM: StoreMannequinActor: SM_WR_Warmaidens_Actor01F: OnBeginState: State: PREBOOT
[09/19/2017 - 03:31:35PM] SM: StoreMannequinActor: SM_WR_Warmaidens_Actor01F: OnEndState: State: PREBOOT
[09/19/2017 - 03:31:35PM] SM: StoreMannequinActor: SM_WR_Warmaidens_Actor01F: OnBeginState: State: INITIALIZING
[09/19/2017 - 03:31:35PM] SM: StoreMannequinActor: SM_WR_Warmaidens_Actor01F: OnEndState: State: INITALIZING
[09/19/2017 - 03:31:35PM] SM: StoreMannequinActor: SM_WR_Warmaidens_Actor01F: OnBeginState: State: RESTOCKING
[09/19/2017 - 03:31:36PM] SM: StoreMannequinActor: SM_WR_Warmaidens_Actor01F: OnEndState: State: RESTOCKING
[09/19/2017 - 03:31:36PM] SM: StoreMannequinActor: SM_WR_Warmaidens_Actor01F: OnBeginState: State: ACTIVE
[09/19/2017 - 03:31:51PM] SM: StoreMannequinActor: SM_WR_Warmaidens_Actor01F: OnActivate: State: ACTIVE

This is the state flow when it fails:

[09/19/2017 - 03:36:36PM] SM: StoreMannequinActor: SM_WR_Warmaidens_Actor01F: OnEndState: State: EMPTY
[09/19/2017 - 03:36:36PM] SM: StoreMannequinActor: SM_WR_Warmaidens_Actor01F: OnBeginState: State: PREBOOT
[09/19/2017 - 03:36:36PM] SM: StoreMannequinActor: SM_WR_Warmaidens_Actor01F: OnEndState: State: PREBOOT
[09/19/2017 - 03:36:36PM] SM: StoreMannequinActor: SM_WR_Warmaidens_Actor01F: OnBeginState: State: INITIALIZING
[09/19/2017 - 03:36:37PM] SM: StoreMannequinActor: SM_WR_Warmaidens_Actor01F: OnEndState: State: INITALIZING
[09/19/2017 - 03:36:37PM] SM: StoreMannequinActor: SM_WR_Warmaidens_Actor01F: OnBeginState: State: RESTOCKING
[09/19/2017 - 03:36:37PM] SM: StoreMannequinActor: SM_WR_Warmaidens_Actor01F: OnEndState: State: RESTOCKING
[09/19/2017 - 03:36:37PM] SM: StoreMannequinActor: SM_WR_Warmaidens_Actor01F: OnBeginState: State: ACTIVE
[09/19/2017 - 03:37:11PM] SM: StoreMannequinActor: SM_WR_Warmaidens_Actor01F: OnActivate: State: EMPTY

*something* is breaking in the active state -- it reaches it, and the OnBeginState code executes, but then it *leaves* it somehow without a. triggering an OnEndState event from it *or* an OnBeginState in any other state, including the empty state.

 

Some of my state changes are being driven by an external script on a control quest. I'll poke at that and see if there's some timing that's breaking.

EDIT:

Delaying the beginning of the state changes by thirty seconds avoids the problem, but doesn't tell me what the problem was to start with...

Edited by foamyesque
Link to comment
Share on other sites

What is the empty state in your scripts, this "" or this "EMPTY"?

 

You should try my approach and write a simple workflow for the whole script. Now you should find out who (external script, internal event, internal function) is changing the state in the script, here probably "StoreMannequinActor.psc" as I can see in the debug output above.

 

I do not believe it is a problem with OnBeginState(). Do you have this event in your script inserted?

;=====================
state ACTIVE
;===========
EVENT OnEndState()
   Debug.Trace("[ACTIVE] OnEndState() - has been reached..")
ENDEVENT
;=======
endState

You wrote: "Delaying the beginning of the state changes by thirty seconds avoids the problem". What state? I cannot see your script code!

Edited by ReDragon2013
Link to comment
Share on other sites

What is the empty state in your scripts, this "" or this "EMPTY"?

 

You should try my approach and write a simple workflow for the whole script. Now you should find out who (external script, internal event, internal function) is changing the state in the script, here probably "StoreMannequinActor.psc" as I can see in the debug output above.

 

I do not believe it is a problem with OnBeginState(). Do you have this event in your script inserted?

;=====================
state ACTIVE
;===========
EVENT OnEndState()
   Debug.Trace("[ACTIVE] OnEndState() - has been reached..")
ENDEVENT
;=======
endState

You wrote: "Delaying the beginning of the state changes by thirty seconds avoids the problem". What state? I cannot see your script code!

 

The default fallback empty state, "". I just have the traces set to say "EMPTY" for ease of reading.

 

I don't think seeing my script code would help much. StoreMannequinActor.psc hits the Creation Kit's maximum character count for scripts and does a lot of things, many of them tied into still other scripts. However, as best as I can tell, none of those are *happening* in the bad-behaviour system.

 

The initial trigger out of the empty state and into initializing processes is called by a control script on a quest, that only sets up initialization if SKSE is present. This is necessary on account of how SKSE functions are absolutely critical to the functioning of the script and no feasible substitutes exist for much of it. I added a large delay to the call to StoreMannequinActor that triggers the shift into Preboot (all subsequent state changes in the log are driven internally), and I *had* thought that had fixed it, but further examination shows that it had not.

 

What does fix it is if the mannequin the script is running on is fully loaded and in the same cell as the player. The rest remain broken. But on the other hand this requirement is not needed if a. the save in question is clean or b. I manually retrigger the entire code path (which I've set up the ability to do); in both of those scenarios, fully loaded or not, same cell or not, everything works. The mannequin actors in question are persistent and have persistence locations set appropriately.

Edited by foamyesque
Link to comment
Share on other sites

Update: I've set up a workaround by means of an OnLoad statement in the empty state that dumps the script back into a working state. In the ordinary course of execution it should be impossible to trigger -- it's overridden in all the others -- but it catches the weird glitch I'm experiencing and corrects it.

 

ReDragon: If you are still interested in the code, the current version is here:

 

[spoiler=Large script]

 

Scriptname foam_SM_StoreMannequinActor extends Actor

import utility

ObjectReference Property SM_MiscEnableMarker Auto
GlobalVariable Property SM_SKSEVersion Auto
GlobalVariable Property SM_MannequinGenders Auto
FormList Property SM_MannequinActorList Auto
FormList Property SM_ShutdownMannequinActorList Auto
FormList Property SM_MannequinGenderOverrideActorList Auto
foam_SM_MerchantChestQuest Property SM_MerchantChestQuest Auto
foam_SM_ControlQuest Property SM_ControlQuest Auto

bool Property bDefaultGender = true Auto
bool Property bReverseEnableStatus = false Auto
Faction Property SM_StoreMannequinFaction Auto
Faction Property JobMerchantFaction Auto
Faction Property myOwningFaction = none Auto
Actor[] Property PossibleOwners Auto
int Property iSaleRadius Auto

Spell Property SM_IsVendingFFSelf Auto
Spell Property SM_IsTalkingFFSelf Auto
Topic Property BarterTopic Auto
FormList Property SM_TalkingActorList Auto
FormList Property SM_VendingActorList Auto

String Property sMyID = "" Auto

FormList Property SM_EmptyFormList Auto
;Keyword Property SM_EnableParent Auto
Keyword Property SM_PairedMannequin Auto

foam_SM_StoreMannequinActor myPairedMannequin = none
;foam_SM_StoreMannequinActor Property myPairedMannequin = none Auto
ObjectReference Property myEnableParent = none Auto

;Inventory control properties
LeveledItem[] Property OutfitList Auto            ;A list of what the mannequin should wear. Each array element is a different outfit.
Quest[] Property QuestCompleteReqs Auto        ;What quest needs to be completed for the matching outfit to be used. This array should never be larger than the OutfitList, but it can be shorter.
bool[] Property bPurchaseFlags Auto                ;Whether the items on the mannequin should be added to the merchant chest
Faction[] Property RequiredFactions Auto        ;Player must be in one of these factions to purchase from the mannequin (if none, no restriction)
Faction[] Property ExcludedFactions Auto        ;Player cannot buy if they are in any of these factions

LeveledItem Property myOutfit = none Auto
bool Property bPurchasable = false Auto
Actor Property PlayerRef Auto
ObjectReference myChest = none
ObjectReference myMarker = none

Faction Property CrimeFaction = none Auto
foam_SM_MerchantChestAlias myChestAlias = none

string sWaitMenu = "Sleep/Wait Menu"
string sMapMenu = "MapMenu"
int kVendorFlag =0x00004000

;Code flow flags
bool bInitialized = false
bool bThieving = false
bool bPopping = false
bool bActivating = false
bool bRestockQueued = false
bool bRestockNoWait = false
bool bGenderUpdateQueued = false
bool bQueueActive = false
bool[] Property bGenderUpdating Auto
bool Property bUsePaired = false Auto

Event OnBeginState()
    Debug.Trace("SM: StoreMannequinActor: " + sMyID + ": OnBeginState: State: EMPTY")
EndEvent

Event OnEndState()
    Debug.Trace("SM: StoreMannequinActor: " + sMyID + ": OnEndState: State: EMPTY")
EndEvent

;polices states
Event OnLoad()
    if SM_ShutdownMannequinActorList.Find(self) < 0
        if GetEnableTargetStatus()
            EnableAI(true)
            MoveTo(myMarker)        
            EnableAI(false)
            GotoState("Active")
        else
            GotoState("Inactive")
        endif
    else
        GotoState("Error")
    endif
EndEvent

Event OnInit()

    Debug.Trace("SM: StoreMannequinActor: OnInit: State: EMPTY")
    if self == none
        ShutDown()
    else
        if bDefaultGender
            bGenderUpdating = new bool[2]
        endif
        SM_MannequinActorList.AddForm(self)
    endif

EndEvent

Function StartUp()
    GotoState("Preboot")
EndFunction

Function ShutDown()
    RegisterForSingleUpdate(0.1)
    GotoState("Error")
EndFunction

;        Preboot State

State PreBoot

Event OnBeginState()
    Debug.Trace("SM: StoreMannequinActor: " + sMyID + ": OnBeginState: State: PREBOOT")
    UnregisterForUpdate()
    RegisterForSingleUpdate(1.0)
EndEvent

Event OnLoad()
EndEvent

Event OnUpdate()
    UnregisterForUpdate()
    GotoState("Initializing")
EndEvent

EndState

;        Initializing State

State Initializing

Event OnBeginState()

    Debug.Trace("SM: StoreMannequinActor: " + sMyID + ": OnBeginState: State: INITIALIZING")

    EnableAI(false)

    if GetBaseAV("CarryWeight") < 10000
        SetAV("CarryWeight", 10000)
    endif

    IgnoreFriendlyHits()

    bool bInitSuccess = true

    myMarker = GetLinkedRef()

    if myMarker != none
        if sMyID == ""
            sMyID = myMarker.GetPositionX() as int + "_" + myMarker.GetPositionY() as int + "_" + myMarker.GetPositionZ() as int + "_" + GetActorBase().GetSex() as int
        endif
    else
        if sMyID == ""
            sMyID = self.GetPositionX() as int + "_" + self.GetPositionY() as int + "_" + self.GetPositionZ() as int + "_" + GetActorBase().GetSex() as int
        endif
    endif

    ;Debug.Trace("SM: StoreMannequinActor: " + sMyID + ": OnBeginState: State: INITIALIZING: ID'ed")    

    if bInitSuccess
        if myMarker == none
            Debug.Trace("SM: StoreMannequinActor: " + sMyID + ": OnBeginState: State: INITIALIZING: ERROR: No linked marker!")
            bInitSuccess = false
        endif
    endif

    if bInitSuccess
        if SM_PairedMannequin == none
            Debug.Trace("SM: StoreMannequinActor: " + sMyID + ": OnBeginState: State: INITIALIZING: ERROR: SM_PairedMannequin keyword not found!")
            bInitSuccess = false
        endif
    endif

    if bInitSuccess
        if myPairedMannequin == none
            myPairedMannequin = GetLinkedRef(SM_PairedMannequin) as foam_SM_StoreMannequinActor
        endif

        if myPairedMannequin == self
            Debug.Trace("SM: StoreMannequinActor: " + sMyID + ": OnBeginState: State: INITIALIZING: ERROR: Paired mannequin is self!")
            bInitSuccess = false
        endif
    endif

    if bInitSuccess
        if myPairedMannequin == none
            if !bDefaultGender
                Debug.Trace("SM: StoreMannequinActor: " + sMyID + ": OnBeginState: State: INITIALIZING: ERROR: No paired mannequin on non-default mannequin!")
                bInitSuccess = false
            else
                Debug.Trace("SM: StoreMannequinActor: " + sMyID + ": OnBeginState: State: INITIALIZING: WARNING: No paired mannequin!")
            endif
        else
            if myPairedMannequin.GetLinkedRef(SM_PairedMannequin) != self
                Debug.Trace("SM: StoreMannequinActor: " + sMyID + ": OnBeginState: State: INITIALIZING: WARNING: Paired mannequin doesn't pair back!")
            endif
        endif
    endif

    if bInitSuccess
        if myPairedMannequin != none
            if bDefaultGender && myPairedMannequin.bDefaultGender
                Debug.Trace("SM: StoreMannequinActor: " + sMyID + ": OnBeginState: State: INITIALIZING: WARNING: Mannequin and paired mannequin both set to default!")        
            elseif !bDefaultGender && !myPairedMannequin.bDefaultGender
                Debug.Trace("SM: StoreMannequinActor: " + sMyID + ": OnBeginState: State: INITIALIZING: WARNING: Mannequin and paired mannequin both set to non-default!")
            elseif !bDefaultGender && myPairedMannequin.bDefaultGender            
                bReverseEnableStatus  = myPairedMannequin.bReverseEnableStatus
                myEnableParent = myPairedMannequin.myEnableParent
                CrimeFaction = myPairedMannequin.CrimeFaction
                myOwningFaction = myPairedMannequin.myOwningFaction
                iSaleRadius = myPairedMannequin.iSaleRadius
                PossibleOwners = myPairedMannequin.PossibleOwners
                bGenderUpdating = myPairedMannequin.bGenderUpdating
                if !OutfitList.Length
                    OutfitList = myPairedMannequin.OutfitList
                endif
                if !QuestCompleteReqs.Length
                    QuestCompleteReqs = myPairedMannequin.QuestCompleteReqs
                endif
                if !bPurchaseFlags.Length
                    bPurchaseFlags = myPairedMannequin.bPurchaseFlags
                endif
                if !RequiredFactions.Length
                    RequiredFactions = myPairedMannequin.RequiredFactions
                endif
                if !ExcludedFactions.Length
                    ExcludedFactions = myPairedMannequin.ExcludedFactions
                endif
            endif
        endif
    endif

    if bInitSuccess
        if iSaleRadius <= 0
            Debug.Trace("SM: StoreMannequinActor: " + sMyID + ": OnBeginState: State: INITIALIZING: ERROR: Invalid sale radius!")
            bInitSuccess = false
        elseif iSaleRadius < 400
            Debug.Trace("SM: StoreMannequinActor: " + sMyID + ": OnBeginState: State: INITIALIZING: WARNING: Sale radius: " + iSaleRadius + ": Low!")
        elseif iSaleRadius > 4000
            Debug.Trace("SM: StoreMannequinActor: " + sMyID + ": OnBeginState: State: INITIALIZING: WARNING: Sale radius: " + iSaleRadius + ": High!")
        endif
    endif

    if bInitSuccess
        if myOwningFaction == none
            Debug.Trace("SM: StoreMannequinActor: " + sMyID + ": OnBeginState: State: INITIALIZING: ERROR: No owning faction!")
            bInitSuccess = false
        endif
    endif
    if bInitSuccess
        if !myOwningFaction.IsFactionFlagSet(kVendorFlag)  
            Debug.Trace("SM: StoreMannequinActor: " + sMyID + ": OnBeginState: State: INITIALIZING: ERROR: Owning faction not vendor!")
            bInitSuccess = false
        endif
    endif
    if bInitSuccess
        myChest = myOwningFaction.GetMerchantContainer()
        if myChest == none
            Debug.Trace("SM: StoreMannequinActor: " + sMyID + ": OnBeginState: State: INITIALIZING: ERROR: No merchant container!")
            bInitSuccess = false
        endif
    endif
    if bInitSuccess
        myChestAlias = SM_MerchantChestQuest.RegisterChest(myChest) as foam_SM_MerchantChestAlias
        if myChestAlias == none
            Debug.Trace("SM: StoreMannequinActor: " + sMyID + ": OnBeginState: State: INITIALIZING: ERROR: No alias provided!")
            bInitSuccess = false
        endif
    endif
    if bInitSuccess

        if CrimeFaction == none
            Debug.Trace("SM: StoreMannequinActor: " + sMyID + ": OnBeginState: State: INITIALIZING: WARNING: No crime faction!")
        else
            SetCrimeFaction(CrimeFaction)
        endif
        
        if !OutfitList.Length
            Debug.Trace("SM: StoreMannequinActor: " + sMyID + ": OnBeginState: State: INITIALIZING: WARNING: No inventory provided!")
        endif

        AddToFaction(SM_StoreMannequinFaction)
        SM_ControlQuest.IncrementMannequins()
        SM_ShutdownMannequinActorList.RemoveAddedForm(self)
        ;Debug.Trace("SM: StoreMannequinActor: " + sMyID + ": OnBeginState: State: INITIALIZING: Initialized: Index: " + SM_MannequinActorList.Find(self))

        if !GetEnableTargetStatus()
            GotoState("Inactive")
        else
            ;Debug.Trace("SM: StoreMannequinActor: " + sMyID + ": OnBeginState: State: INITIALIZING: Disabled: " + IsDisabled())
            if IsDisabled()
                Enable()
            endif
            ;Debug.Trace("SM: StoreMannequinActor: " + sMyID + ": OnBeginState: State: INITIALIZING: Disabled: " + IsDisabled())

            if Is3DLoaded()
                RegisterForMenu(sWaitMenu)
                RegisterForMenu(sMapMenu)
            endif

            GotoState("Restocking")

        endif
        
    endif
    
    if !bInitSuccess    
        Debug.Trace("SM: StoreMannequinActor: " + sMyID + ": OnBeginState: State: INITIALIZING: ERROR: Initalizing failed!")
        SM_MannequinActorList.RemoveAddedForm(self)
        ShutDown()
    endif

EndEvent

;keeps any update calls until a working state can process or reject them
Event OnUpdate()
    RegisterForSingleUpdate(0.5)
EndEvent

LeveledItem Function GetOutfit(bool abUsePaired = false)
    return none
EndFunction

bool Function GetPurchasable(bool abUsePaired = false)
    return false
EndFunction

Event OnLoad()
EndEvent

EndState


;        Error State

State Error

Event OnBeginState()
    Debug.Trace("SM: StoreMannequinActor:  " + sMyID + ": OnBeginState: State: ERROR")
EndEvent

Event OnUpdate()

    Debug.Trace("SM: StoreMannequinActor: " + sMyID + ": CRITICAL ERROR: Disabling!")

    UnregisterForUpdate()
    UnregisterForUpdateGameTime()

    if self != none
        SM_ShutdownMannequinActorList.AddForm(self)
        SM_ControlQuest.DecrementMannequins()
        if SM_ControlQuest.bModActiveStatusTarget
            Debug.Trace("SM: StoreMannequinActor: " + sMyID + ": OnUpdate: State: ERROR: Index: " + SM_MannequinActorList.Find(self) + ": Shutdown count: " + SM_ShutdownMannequinActorList.GetSize())
        endif
        UnregisterForLOS(PlayerRef, self)

        if SM_SKSEVersion.GetValueInt() > 0
            UnregisterForMenu(sWaitMenu)
            UnregisterForMenu(sMapMenu)
        endif

        DisableNoWait(false)
        RemoveFromFaction(SM_StoreMannequinFaction)

    endif

    RemoveAllInventoryEventFilters()

    bPurchasable = false
    bGenderUpdateQueued = false
    bRestockQueued = false
    bRestockNoWait = false

EndEvent

;Overrides dangerous functions/events from empty state

Event OnLoad()
EndEvent

Function RegisterForSingleUpdate(Float afInterval)
EndFunction

Function RegisterForSingleUpdateGameTime(Float afInterval)
EndFunction

Function RegisterForUpdate(Float afInterval)
EndFunction

Function RegisterForUpdateGameTime(Float afInterval)
EndFunction

Function UpdateList()
EndFunction

Actor Function GetOwner(bool bFullCheck = true)
    return none
EndFunction

Function PopToMarker()
EndFunction

Function EquipAll()
EndFunction

bool Function GetEnableTargetStatus()
    return false
EndFunction

bool Function QueueUpdateGender(bool abChangeGender = false)
    return false
EndFunction

LeveledItem Function GetOutfit(bool abUsePaired = false)
    return none
EndFunction

bool Function GetPurchasable(bool abUsePaired = false)
    return false
EndFunction

EndState


;        Inactive State

State Inactive

Event OnBeginState()
    Debug.Trace("SM: StoreMannequinActor: " + sMyID + ": OnBeginState: State: INACTIVE")

    bRestockQueued = false
    bRestockNoWait = false
    UnregisterForLOS(PlayerRef, self)

    UnregisterForMenu(sWaitMenu)
    UnregisterForMenu(sMapMenu)

    BlockActivation(true)

    if IsEnabled()
        PopToMarker()
        DisableNoWait()
    endif

    UpdateList()

EndEvent

Event OnEndState()
    Debug.Trace("SM: StoreMannequinActor: " + sMyID + ": OnEndState: State: INACTIVE")
EndEvent

;Fires even when disabled
Event OnCellAttach()
    ;Debug.Trace("SM: StoreMannequinActor: " + sMyID + ": OnCellAttach: State: INACTIVE")
    if GetEnableTargetStatus()
        if IsDisabled()
            EnableNoWait()
        endif
        GotoState("Restocking")
    endif
EndEvent

Event OnUpdate()

    ;Debug.Trace("SM: StoreMannequinActor: " + sMyID + ": OnUpdate: State: INACTIVE: Restock queued: " + bRestockQueued + ": Gender update queued: " + bGenderUpdateQueued)
    if bRestockQueued
        bRestockQueued = false
        bRestockNoWait = false
    endif
    if bGenderUpdateQueued
        if GetEnableTargetStatus()
            GotoState("Active")
        endif
        bGenderUpdating[bDefaultGender as int] = false
        bGenderUpdateQueued = false
    endif

EndEvent

Event OnLoad()
EndEvent

LeveledItem Function GetOutfit(bool abUsePaired = false)
    return none
EndFunction

bool Function GetPurchasable(bool abUsePaired = false)
    return false
EndFunction

Function EquipAll()
EndFunction

EndState


;        Restocking State

State Restocking

Event OnBeginState()

    Debug.Trace("SM: StoreMannequinActor: " + sMyID + ": OnBeginState: State: RESTOCKING")

    bUsePaired = false
    bRestockNoWait = false

    BlockActivation(true)
    RemoveAllItems()
    AddDisplayItems()
    EquipAll()

    bRestockQueued = false
    bQueueActive = true
    RegisterForSingleUpdate(0.1)

EndEvent

Event OnEndState()
    Debug.Trace("SM: StoreMannequinActor: " + sMyID + ": OnEndState: State: RESTOCKING")
EndEvent

Event OnUpdate()

    if bQueueActive
        bQueueActive = false
        GotoState("Active")
    else
        if bGenderUpdateQueued
            RegisterForSingleUpdate(0.5)
        endif
        if bRestockQueued
            bRestockQueued = false
            bRestockNoWait = false
        endif
    endif

EndEvent

Event OnLoad()
    ;Debug.Trace("SM: StoreMannequinActor: " + sMyID + ": OnLoad: State: RESTOCKING")

    RegisterForMenu(sWaitMenu)
    RegisterForMenu(sMapMenu)

EndEvent

Event OnUnload()
    Debug.Trace("SM: StoreMannequinActor: " + sMyID + ": OnUnLoad: State: RESTOCKING")

    UnregisterForLOS(PlayerRef, self)
    if SM_SKSEVersion.GetValueInt() > 0
        UnregisterForMenu(sWaitMenu)
        UnregisterForMenu(sMapMenu)
    endif
EndEvent

Event OnMenuClose(string asMenuName)
    ;Debug.Trace("SM: StoreMannequinActor: " + sMyID + ": OnMenuClose: State: RESTOCKING")
    RegisterForSingleLOSGain(PlayerRef, self)
EndEvent

Event OnGainLOS(Actor akViewer, ObjectReference akTarget)
    ;Debug.Trace("SM: StoreMannequinActor: " + sMyID + ": OnGainLOS: State: RESTOCKING")
    PopToMarker()
EndEvent

;Adds mannequin's default display items
Function AddDisplayItems()

    myOutfit = GetOutfit()

    bPurchasable = GetPurchasable()
    SetFactionRank(SM_StoreMannequinFaction, bPurchasable as int)    
    ;Debug.Trace("SM: StoreMannequinActor: " + sMyID +": AddDisplayItems: State: RESTOCKING: Purchasable: " + (bPurchasable as int) + ": Faction rank: " + GetFactionRank(SM_StoreMannequinFaction))

    ;Adds the items from the leveled list    
    if myOutfit != none
        AddItem(myOutfit)
    endif

    ;Updates merchant container
    UpdateList()

EndFunction


EndState


;        Active State

State Active

Event OnBeginState()
    Debug.Trace("SM: StoreMannequinActor: " + sMyID + ": OnBeginState: State: ACTIVE")
    PopToMarker()
    if IsDisabled()
        Enable()
    endif
    PopToMarker()
    if Is3DLoaded()
        OnLoad()
    endif
    BlockActivation(false)
    if bRestockQueued || bGenderUpdateQueued
        OnUpdate()
    endif
EndEvent

Event OnEndState()
    Debug.Trace("SM: StoreMannequinActor: " + sMyID + ": OnEndState: State: ACTIVE")
EndEvent

;Fires even when disabled
Event OnCellAttach()
    ;Debug.Trace("SM: StoreMannequinActor: " + sMyID + ": OnCellAttach: State: ACTIVE")
    if GetEnableTargetStatus()
        PopToMarker()
        if GetOutfit(bUsePaired) == myOutfit

            if bPurchasable != GetPurchasable(bUsePaired)
                bPurchasable = !bPurchasable
                SetFactionRank(SM_StoreMannequinFaction, bPurchasable as int)
                UpdateList()
            endif

        endif
    else
        GotoState("Inactive")    
    endif
EndEvent

Event OnLoad()
    ;Debug.Trace("SM: StoreMannequinActor: " + sMyID + ": OnLoad: State: ACTIVE")

    RegisterForMenu(sWaitMenu)
    RegisterForMenu(sMapMenu)

EndEvent

Event OnUnload()
    ;Debug.Trace("SM: StoreMannequinActor: " + sMyID + ": OnUnLoad: State: ACTIVE")

    UnregisterForLOS(PlayerRef, self)
    UnregisterForMenu(sWaitMenu)
    UnregisterForMenu(sMapMenu)

EndEvent

Event OnMenuClose(string asMenuName)
    ;Debug.Trace("SM: StoreMannequinActor: " + sMyID + ": OnMenuClose: State: ACTIVE")
    RegisterForSingleLOSGain(PlayerRef, self)
EndEvent

Event OnGainLOS(Actor akViewer, ObjectReference akTarget)
    ;Debug.Trace("SM: StoreMannequinActor: " + sMyID + ": OnGainLOS: State: ACTIVE")
    PopToMarker()
EndEvent

Event OnUpdate()
    ;Debug.Trace("SM: StoreMannequinActor: " + sMyID + ": OnUpdate: State: ACTIVE")
    if bRestockQueued
        if !IsNearPlayer() || bRestockNoWait
            ;Debug.Trace("SM: StoreMannequinActor: " + sMyID + ": OnUpdate: State: ACTIVE: Triggering restock")
            if bGenderUpdateQueued
                RegisterForSingleUpdate(0.5)
            endif    
            GotoState("Restocking")
        else
            ;Debug.Trace("SM: StoreMannequinActor: " + sMyID + ": OnUpdate: State: ACTIVE: Restock postponed, player near.")
            if bGenderUpdateQueued
                UpdateGender()
            else
                RegisterForSingleUpdate(2.0)
            endif
        endif
    elseif bGenderUpdateQueued
        UpdateGender()
    endif

EndEvent

;Forces mannequin to its marker and opens the inventory screen for item management
Event OnActivate(ObjectReference akTriggerRef)

    Debug.Trace("SM: StoreMannequinActor: " + sMyID + ": OnActivate: State: ACTIVE")    
    if !bActivating && !IsActivationBlocked()

        bActivating = true
        PopToMarker()
    
        if akTriggerRef == PlayerRef
            bThieving = false
            if PlayerRef.IsSneaking()
                bThieving = true
            endif

            if GetOutfit(bUsePaired) == myOutfit

                if bPurchasable != GetPurchasable(bUsePaired)
                    bPurchasable = !bPurchasable        ;Update list checks the purchase status, so set it first
                    SetFactionRank(SM_StoreMannequinFaction, bPurchasable as int)
                    UpdateList()
                endif
            endif

            if bThieving
            else

                Actor myOwner = GetOwner()

                if myOwner != none
                    myOwner.Say(BarterTopic)
                else
                    Debug.Notification("No merchant available")
                endif
            endif
        endif

        bActivating = false

    endif

EndEvent


Event OnItemAdded(Form akBaseItem, int aiItemCount, ObjectReference akItemReference, ObjectReference akSourceContainer)
    
    if akSourceContainer ==  none
        UpdateList()
    else
        ;if bThieving
            AddInventoryEventFilter(SM_EmptyFormList)
            if akItemReference != none
                RemoveItem(akItemReference, aiItemCount, true, akSourceContainer)
            else
                RemoveItem(akBaseItem, aiItemCount, true, akSourceContainer)
            endif
            RemoveInventoryEventFilter(SM_EmptyFormList)
        ;endif
    endif

EndEvent

Event OnItemRemoved(Form akBaseItem, int aiItemCount, ObjectReference akItemReference, ObjectReference akDestContainer)
    ;Debug.Trace("SM: StoreMannequinActor: " + sMyID + ":  Item: " + akBaseItem.GetName() + ": Removed")
    QueueNiNodeUpdate()
    UpdateList()
EndEvent

Function RemoveRegisteredItem(Form akRemoveForm = none, int aiCount = 1)
    if akRemoveForm
        AddInventoryEventFilter(SM_EmptyFormList)
        RemoveItem(akRemoveForm, aiCount, true)
        RemoveInventoryEventFilter(SM_EmptyFormList)
        QueueNiNodeUpdate()
    endif
EndFunction

bool Function UpdateGender()
    ;Debug.Trace("SM: StoreMannequinActor: " + sMyID + ": OnUpdate: State: ACTIVE: Updating gender")
    bool bChanged = false
    bool bActive = GetEnableTargetStatus()
    bGenderUpdating[bDefaultGender as int] = false
    int iLoops = 0
    while bGenderUpdating[0] || bGenderUpdating[1]
        iLoops+=1
        ;Debug.Trace("SM: StoreMannequinActor: " + sMyID + ": OnUpdate: UpdateGender: Flags: " + bGenderUpdating[0] + " : " + bGenderUpdating[1] + " Loop count: " + iLoops)
        WaitMenuMode(0.1)
    endwhile
    if !bActive && myPairedMannequin != none
        if myPairedMannequin.GetEnableTargetStatus()
            iLoops = 0
            while !myPairedMannequin.IsEnabled()
                iLoops += 1
                ;Debug.Trace("SM: StoreMannequinActor: " + sMyID + ": UpdateGender: Flags: " + bGenderUpdating[0] + " : " + bGenderUpdating[1] + " Loop count: " + iLoops)
                WaitMenuMode(0.1)
            endwhile
            TransferItems()
            myPairedMannequin.EquipAll()
            GotoState("Inactive")
        endif
        bChanged = true
    endif
    if bRestockQueued
        ;Debug.Trace("SM: StoreMannequinActor: " + sMyID + ": UpdateGender: Restock flag: " + bRestockQueued)
        RegisterForSingleUpdate(0.5)
    endif
    bGenderUpdateQueued = false
    return bChanged
EndFunction

bool Function RestockInventory(bool abNoWait = false)
    ;Debug.Trace("SM: StoreMannequinActor: " + sMyID + ": Restock: Called")
    if !GetEnableTargetStatus()
        GotoState("Inactive")
    else
        if GetOwner(false) == none
            Debug.Trace("SM: StoreMannequinActor: " + sMyID + ": Restock: WARNING: No owner found")
        else
            bRestockQueued = true
            bRestockNoWait = abNoWait
            ;Debug.Trace("SM: StoreMannequinActor: " + sMyID + ": Restock: Queued")
            RegisterForSingleUpdate(0.1)
            return true
        endif
    endif
    return false
EndFunction

bool Function TransferItems()
    if myPairedMannequin == none
        Debug.Trace("SM: StoreMannequinActor: TransferItems: WARNING: No paired mannequin!")
        return false
    else
        if self.IsDisabled()
            Debug.Trace("SM: StoreMannequinActor: " + sMyID + ": TransferItems: WARNING: Mannequin: " + sMyID + ": Disabled!")        
        else
            if myPairedMannequin.IsDisabled()
                Debug.Trace("SM: StoreMannequinActor:  " + sMyID + ": TransferItems: WARNING: Mannequin: " + myPairedMannequin.sMyID + ": Disabled!")        
            else
                ;copies data to paired mannequin
                myPairedMannequin.bPurchasable = bPurchasable
                myPairedMannequin.SetFactionRank(SM_StoreMannequinFaction, bPurchasable as int)
                myPairedMannequin.myOutfit = myOutfit
                myPairedMannequin.bUsePaired = true
                bUsePaired = false

                ;disables OnItemAdded/Removed events, transfers items, then re-enables the events
                myPairedMannequin.AddInventoryEventFilter(SM_EmptyFormList)
                self.AddInventoryEventFilter(SM_EmptyFormList)

                RemoveAllItems(myPairedMannequin, false, true)

                self.RemoveInventoryEventFilter(SM_EmptyFormList)
                myPairedMannequin.RemoveInventoryEventFilter(SM_EmptyFormList)

                myPairedMannequin.UpdateList()
                self.UpdateList()

                return true
            endif
        endif
    endif
EndFunction

EndState


;        Empty State

Function EquipAll()

    Form[] myInventory = GetContainerForms()
    int iLength = myInventory.Length
    int i = iLength

    if iLength > 1

        int[] iPriorityList = CreateIntArray(i)

        while i > 0
            i-=1
            iPriorityList[i] = CalcPriority(myInventory[i])
        endwhile

        ;insertion sort
        i=1
        while i < iLength
            int iTempPriority = iPriorityList[i]
            Form TempForm = myInventory[i]
            int j = i - 1
            while j >= 0 && iPriorityList[j] > iTempPriority
                iPriorityList[j+1] = iPriorityList[j]
                myInventory[j+1] = myInventory[j]
                j-=1
            endwhile
            iPriorityList[j+1] = iTempPriority
            myInventory[j+1] = TempForm
            i+=1
        endwhile

    endif
    
    ;Debug.Trace("SM: StoreMannequinActor: " + sMyID + ": EquipAll: Items to equip: " + i)

    bool myGender = GetActorBase().GetSex()

    while i > 0
        i-=1

        ;makes sure only armour is going to be equipped
        Armor testArmor = myInventory[i] as Armor
        if testArmor != none

            ;makes sure only armour with visible models are equipped
            int j = testArmor.GetNumArmorAddons()
            bool bPathsFound = true
            while j > 0
                j-=1
                ArmorAddon testAddon = testArmor.GetNthArmorAddon(j)
                if testAddon.GetModelPath(false, false) == ""
                    if !myGender
                        bPathsFound = false
                    elseif testAddon.GetModelPath(false, true) == ""
                        bPathsFound = false
                    endif
                endif
            endwhile
            ;Debug.Trace("SM: StoreMannequinActor: " + sMyID + ": EquipAll: Item: " + i + ": Equipping: " + myInventory[i].GetName())
            if bPathsFound
                EquipItem(myInventory[i], true)        
            endif
        endif
    endwhile

EndFunction

;Moves the mannequin to its assigned XMarkerHeading
Function PopToMarker()

    if !bPopping
        bPopping = true
        ;Debug.Trace("SM: StoreMannequinActor: " + sMyID + ": PopToMarker: Disabled: " + IsDisabled() + ": 3DLoaded: " + Is3DLoaded())
        MoveTo(myMarker)
        QueueNiNodeUpdate()
        bPopping = false
    endif

EndFunction

;finds the mannequins owner(s) and, optionally, if they are available to sell merchandise.
Actor Function GetOwner(bool bFullCheck = true)

    if myOwningFaction == none
        return none
    endif

    Actor myOwner = none
    int i = PossibleOwners.Length

    while i > 0
        i-=1
        if PossibleOwners[i] == none
            ;Debug.Trace("SM: StoreMannequinActor: " + sMyID + ": GetOwner: Index: " + i + ": NULL")
        else
            string ownerName = PossibleOwners[i].GetDisplayName()
            if !PossibleOwners[i].IsDead() && !PossibleOwners[i].IsDisabled()
                if PossibleOwners[i].GetFactionRank(JobMerchantFaction) >= 0 && PossibleOwners[i].GetFactionRank(myOwningFaction) >= 0
                    if bFullCheck
                        ;Debug.Trace("SM: StoreMannequinActor: " + sMyID + ": GetOwner: Actor: " + ownerName + ": Activation blocked: " + PossibleOwners[i].IsActivationBlocked())
                        if !PossibleOwners[i].IsActivationBlocked()
                            ;Debug.Trace("SM: StoreMannequinActor: " + sMyID + ": GetOwner: Actor: " + ownerName + ": Distance to player: " + PossibleOwners[i].GetDistance(PlayerRef))
                            if PossibleOwners[i].GetDistance(PlayerRef) < iSaleRadius
                                ;Debug.Trace("SM: StoreMannequinActor: " + sMyID + ": GetOwner: Actor: " + ownerName + ": In combat: " + PossibleOwners[i].IsInCombat())
                                if !PossibleOwners[i].IsInCombat()
                                    SM_IsVendingFFSelf.Cast(PossibleOwners[i], PossibleOwners[i])
                                    SM_IsTalkingFFSelf.Cast(PossibleOwners[i], PossibleOwners[i])
                                    ;Debug.Trace("SM: StoreMannequinActor: " + sMyID + ": GetOwner: Actor: " + ownerName + ": Is vending: " + SM_VendingActorList.HasForm(PossibleOwners[i]))
                                    ;Debug.Trace("SM: StoreMannequinActor: " + sMyID + ": GetOwner: Actor: " + ownerName + ": Is talking: " + SM_TalkingActorList.HasForm(PossibleOwners[i]))
                                    WaitMenuMode(0.05)
                                    if SM_VendingActorList.HasForm(PossibleOwners[i]) && !SM_TalkingActorList.HasForm(PossibleOwners[i])
                                        myOwner = PossibleOwners[i]
                                        ;Debug.Trace("SM: StoreMannequinActor: " + sMyID + ": GetOwner: Owner: " + ownerName)
                                    endif
                                    PossibleOwners[i].DispelSpell(SM_IsTalkingFFSelf)
                                    PossibleOwners[i].DispelSpell(SM_IsVendingFFSelf)
                                endif
                            endif
                        endif
                    else
                        myOwner = PossibleOwners[i]
                    endif
                endif
            endif
        endif
    endwhile
        
    return myOwner

EndFunction

;sends mannequin inventory data to the merchant chest script
Function UpdateList()

    ;Debug.Trace("SM: StoreMannequinActor: " + sMyID + ": UpdateList: Purchasable: " + bPurchasable)
    if bPurchasable
        Form[] myInventory = GetContainerForms()
        myChestAlias.UpdateLists(myInventory, self)
    else
        myChestAlias.UpdateLists(CreateFormArray(0), self)
    endif
EndFunction

;Loops through the list of quests
;The last completed quest (or blank value in the list) will be used as the index number
;to determine which leveled list is added and/or purchase flag used. If no list exists, the index is zero
int Function GetIndex()

    int outfit_index = 0
    int i = 0

    if QuestCompleteReqs.Length
    
        while i < QuestCompleteReqs.Length
            if QuestCompleteReqs[i] != none
                if QuestCompleteReqs[i].IsCompleted()
                    outfit_index = i
                endif
            else
                outfit_index = i
            endif
            i+=1
        endwhile
    endif
    
    return outfit_index
    
EndFunction

;Calculates the priority of equipping a given item
int Function CalcPriority(Form akForm)

    int iPriority = 0x000
    if akForm == none
        return iPriority
    endif

    if akForm as Armor
        Armor testArmor = akForm as Armor
        int iSlotMask= testArmor.GetSlotMask()
        int iTestSlot = 0x01
        while iTestSlot < testArmor.kSlotMask61
            if Math.LogicalAnd(iTestSlot, iSlotMask)
                ;chest armour
                if iTestSlot == testArmor.kSlotMask32
                    iPriority+=0x800
                endif

                ;head slots
                if iTestSlot == testArmor.kSlotMask30 || iTestSlot == testArmor.kSlotMask31 || iTestSlot == testArmor.kSlotMask41 || iTestSlot == testArmor.kSlotMask42 || iTestSlot == testArmor.kSlotMask43
                    iPriority+=0x080
                endif

                ;slot count
                iPriority+= 0x010
            endif
            iTestSlot *= 2
        endwhile
    endif

    ;gold value as tiebreaker
    int iValue = (akForm.GetGoldValue() / 10) as int
    if iValue > 0x080
        iValue = 0x080
    endif
    iPriority += iValue
    
    return iPriority
EndFunction

bool Function GetEnableTargetStatus()
    bool bGenderMatch = false
    if !SM_MiscEnableMarker.IsEnabled()
        return false
    endif
    if myPairedMannequin == none
        bGenderMatch = true
    else
        int iMannequinGenders = (SM_MannequinGenders.GetValue() as int)
        if iMannequinGenders == 0
            bool bOverride = SM_MannequinGenderOverrideActorList.HasForm(self)
            if (bDefaultGender && !bOverride) || (!bDefaultGender && bOverride)
                bGenderMatch = true
            endif
        elseif iMannequinGenders == 1 && GetActorBase().GetSex() == 0
            bGenderMatch = true
        elseif iMannequinGenders == 2 && GetActorBase().GetSex() == 1
            bGenderMatch = true
        endif
    endif
    if bGenderMatch == false
        return false
    endif
    if myEnableParent == none
        return true
    endif
    ;WaitMenuMode(0.1)
    if myEnableParent.IsEnabled() && !bReverseEnableStatus
        return true
    elseif !myEnableParent.IsEnabled() && bReverseEnableStatus
        return true
    endif
    return false
EndFunction

bool Function QueueUpdateGender(bool abChangeGender = false)
    while bGenderUpdateQueued
        WaitMenuMode(0.1)
    endwhile
    bGenderUpdateQueued = true
    bGenderUpdating[bDefaultGender as int] = true
    UnRegisterForUpdate()
    RegisterForSingleUpdate(0.1)
    return true
EndFunction

LeveledItem Function GetOutfit(bool abUsePaired = false)

    foam_SM_StoreMannequinActor testMannequin = self
    if abUsePaired
        testMannequin = myPairedMannequin
    endif

    int iQuestIndex = testMannequin.GetIndex()
    LeveledItem testOutfit = none

    if iQuestIndex < testMannequin.OutfitList.Length
        testOutfit = testMannequin.OutfitList[iQuestIndex]
    endif
    
    return testOutfit
EndFunction

bool Function GetPurchasable(bool abUsePaired = false)
    foam_SM_StoreMannequinActor testMannequin = self
    if abUsePaired
        testMannequin = myPairedMannequin
    endif
    bool bPurchaseFlag = false
    if !PlayerExcluded()
        if PlayerIncluded()
            int iQuestIndex = testMannequin.GetIndex()
            if iQuestIndex < testMannequin.bPurchaseFlags.Length
                bPurchaseFlag = testMannequin.bPurchaseFlags[iQuestIndex]
            else
                bPurchaseFlag = true
            endif
        endif
    endif

    return bPurchaseFlag
EndFunction

bool Function PlayerIncluded()

    int i = RequiredFactions.Length
    if i == 0
        return true
    endif
    while i > 0
        i-=1
        ;Debug.Trace("SM: StoreMannequinActor: PlayerIncluded: Index: " + i + ": " + PlayerRef.IsInFaction(RequiredFactions[i]))
        if PlayerRef.IsInFaction(RequiredFactions[i])
            return true
        endif
    endwhile
    return false

EndFunction

bool Function PlayerExcluded()

    int i = ExcludedFactions.Length
    while i > 0
        i-=1
        if PlayerRef.IsInFaction(ExcludedFactions[i])
            return true
        endif
    endwhile
    return false

EndFunction

;-------------------------
;    Null functions
;-------------------------

Function AddDisplayItems()
EndFunction

bool Function RestockInventory(bool abNoWait = false)
    Debug.Trace("SM: StoreMannequinActor: RestockInventory: State: EMPTY: NoWait flag: " + abNoWait)
    return false
EndFunction

Function RemoveRegisteredItem(Form akRemoveForm = none, int aiCount = 1)
EndFunction

bool Function UpdateGender()
    return false
EndFunction

bool Function TransferItems()
    Debug.Trace("SM: StoreMannequinActor: TransferItems: WARNING: Called outside of active state!")
    return false
EndFunction

 

 

 

It's something like twelve hundred lines of sparsely documented interdependent asynchronous spaghetti. Have fun :D

Link to comment
Share on other sites

[ ...] if that game is *not* the first one loaded in the session [...]

 

That's the problem. You're loading a saved game from somewhere other than the main menu right after the game starts. Don't do that if you value your saved games.

 

Loading a save after you've already been playing will almost always corrupt your game in some way. Too many parts of the game simply aren't reinitialized when you do that so you end up with a weird merge of states of the game you were running and the one you are loading. The next time you save you're saving that corrupt mess.

 

The fact that your script works in every situation except that problem case means it's not your script that's the problem it's the overall game state.

Link to comment
Share on other sites

Unfortunately there was a flaw in my testing system -- I'd added a big enough delay that I could coc into a cell before the mannequins loaded in, which changed my results. I now get the same failure however the save game is loaded, though not with all save games.

 

Workaround notwithstanding I'm still puzzled as to what could possibly be changing the state without tripping any of the state transition events.

Edited by foamyesque
Link to comment
Share on other sites

Your script code is very complicated imho. It took me some time to understand what is going on here, additional you are using SKSE function sometimes without checking presence of SKSE, not so good.

Maybe you can use next code to find the mistake. I didn't compiled the script. Do not forget my English is weak and ot really native written.

 

foam_SM_StoreMannequinActor

 

Scriptname foam_SM_StoreMannequinActor extends Actor
{rewritten by ReDragon 2017}    ; "Ordnung ist das halbe Leben!"

  foam_SM_ControlQuest        PROPERTY SM_ControlQuest       auto        ; use the quest property !!
  foam_SM_MerchantChestQuest  PROPERTY SM_MerchantChestQuest auto        ; use the quest property !!
  foam_SM_MerchantChestAlias  myChestAlias
  foam_SM_StoreMannequinActor myPairedMannequin

  Quest[] PROPERTY QuestCompleteReqs auto            ; What quest needs to be completed for the matching outfit to be used.
  ; This array should never be larger than the OutfitList, but it can be shorter.

  LeveledItem   PROPERTY myOutfit   auto Hidden
  LeveledItem[] PROPERTY OutfitList auto              ; A list of what the mannequin should wear. Each array element is a different outfit.

  GlobalVariable PROPERTY SM_SKSEVersion      auto
  GlobalVariable PROPERTY SM_MannequinGenders auto

 ;Keyword PROPERTY SM_EnableParent    auto                ; removed by default
  Keyword PROPERTY SM_PairedMannequin auto

  Topic PROPERTY BarterTopic auto

  FormList PROPERTY SM_MannequinActorList               auto
  FormList PROPERTY SM_ShutdownMannequinActorList       auto
  FormList PROPERTY SM_MannequinGenderOverrideActorList auto
  FormList PROPERTY SM_TalkingActorList auto
  FormList PROPERTY SM_VendingActorList auto
  FormList PROPERTY SM_EmptyFormList    auto

  Spell PROPERTY SM_IsVendingFFSelf auto
  Spell PROPERTY SM_IsTalkingFFSelf auto

  Faction[] PROPERTY RequiredFactions         auto   ; Player must be in one of these factions to purchase from the mannequin (if none, no restriction)
  Faction[] PROPERTY ExcludedFactions         auto   ; Player cannot buy if they are in any of these factions
  Faction   PROPERTY SM_StoreMannequinFaction auto
  Faction   PROPERTY JobMerchantFaction       auto
  Faction   PROPERTY myOwningFaction          auto Hidden
  Faction   PROPERTY CrimeFaction             auto Hidden

  Actor[] PROPERTY PossibleOwners auto
 ;Actor   PROPERTY PlayerRef      auto

  ObjectReference PROPERTY SM_MiscEnableMarker auto
  ObjectReference PROPERTY myEnableParent      auto Hidden
 ;ObjectReference myChest
 ;ObjectReference myMarker


  Bool[] PROPERTY bPurchaseFlags        auto        ; whether the items on the mannequin should be added to the merchant chest
  Bool[] PROPERTY bGenderUpdating       auto Hidden
  Bool   PROPERTY bUsePaired            auto
  Bool   PROPERTY bPurchasable          auto
  Bool   PROPERTY bDefaultGender = TRUE auto        ; set TRUE
  Bool   PROPERTY bReverseEnableStatus  auto

;Code flow flags
  Bool bInitialized            ; it is False by default
 ;Bool bThieving
  Bool bPopping
  Bool bActivating
  Bool bRestockQueued
  Bool bRestockNoWait
  Bool bQueueActive
  Bool bGenderUpdateQueued

  Int PROPERTY iSaleRadius auto
 ;Int kVendorFlag = 0x00004000

  String PROPERTY sMyID auto                        ; "", empty by default


; -- EVENTs --

EVENT OnInit()
IF self.GetBaseObject()
ELSE
    shutdown()
    RETURN    ; - STOP -
ENDIF
;---------------------
    Debug.Trace("SM: OnInit() - has been reached..  " +self)
    SM_MannequinActorList.AddForm(self)
    IF ( bDefaultGender )
        bGenderUpdating = new Bool[2]
    ENDIF
ENDEVENT


EVENT OnCellAttach()
; Fires event only when disabled

    string s = self.GetState()
    Debug.Trace("SM: StoreMannequinActor: " + sMyID + ": OnCellAttach: State: " +s)

IF (s == "Inactive")
    IF GetEnableTargetStatus()
        IF self.IsDisabled()
            self.EnableNoWait()                            ; triggers OnLoad() in same state
        ENDIF
        gotoState("Restocking")        ; ### STATE ###  (R)
        myF_InitRestock()
    ENDIF
    RETURN    ; - STOP -
ENDIF
;---------------------
IF (s == "Active")
    IF GetEnableTargetStatus()
    ELSE
        gotoState("Inactive")        ; ### STATE ###  (I)
        myF_InitInactive()
        RETURN    ; - STOP -
    ENDIF
;    ----------------------
    PopToMarker()

    IF (GetOutfit(bUsePaired) == myOutfit)
        IF (GetPurchasable(bUsePaired) == bPurchasable)
        ELSE
            bPurchasable = !bPurchasable
            self.SetFactionRank(SM_StoreMannequinFaction, bPurchasable as Int)
            UpdateList()
        ENDIF
    ENDIF
ENDIF
ENDEVENT


EVENT OnLoad()
    int i = SM_ShutdownMannequinActorList.Find(self)

IF (i >= 0)
    gotoState("Error")                ; ### STATE ###  - E -
    RETURN    ; - STOP -    already in shutdown list
ENDIF
;---------------------
IF GetEnableTargetStatus()
ELSE
    gotoState("Inactive")            ; ### STATE ###  (I)
    myF_InitInactive()
    RETURN    ; - STOP -    status disabled
ENDIF
;---------------------
    self.EnableAI(TRUE)
    self.MoveTo( self.GetLinkedRef() )                        ; myMarker
    self.EnableAI(False)
    gotoState("Active")                ; ### STATE ###  (A)
    myF_InitActive()
ENDEVENT


;;================================
;state Preboot    ; not really required
;;============
;EVENT OnBeginState()
;   Debug.Trace("SM: StoreMannequinActor: " + sMyID + ": OnBeginState: State: Preboot")
;ENDEVENT
;;=======
;endState


;================================
state Error        ; prevent some events from running
;==========
EVENT OnCellAttach()
ENDEVENT

EVENT OnLoad()
ENDEVENT

EVENT OnUpdate()        ; maybe, maybe not?
ENDEVENT

FUNCTION shutDown()        ; make sure to run this once only
ENDFUNCTION
;=======
endState


;------------------
FUNCTION shutDown()
;------------------
    UnregisterForUpdate()
;;;    UnregisterForUpdateGameTime()

    gotoState("Error")                ; ### STATE ###  - E -

    SM_MannequinActorList.RemoveAddedForm(self)
    Debug.Trace("SM: OnUpdate() [Initializing] failed!!  shutDown for " +sMyID)

IF ( self )
    UnregisterForLOS(Game.GetPlayer(), self as ObjectReference)
    myF_RFM(False)

    SM_ShutdownMannequinActorList.AddForm(self)
    SM_ControlQuest.DecrementMannequins()

    self.DisableNoWait()
    self.RemoveFromFaction(SM_StoreMannequinFaction)

    IF ( SM_ControlQuest.bModActiveStatusTarget )
        int i = SM_MannequinActorList.Find(self)
        int c = SM_ShutdownMannequinActorList.GetSize()
        Debug.Trace("SM: OnUpdate() [Initializing] Index: " +i+ ", Shutdown count: " +c+ " " +sMyID)
    ENDIF
ENDIF

    self.RemoveAllInventoryEventFilters()

    bPurchasable        = False
    bGenderUpdateQueued = False
    bRestockQueued      = False
    bRestockNoWait      = False
ENDFUNCTION


;----------------------
FUNCTION myF_InitGoto()
;----------------------
IF GetEnableTargetStatus()
ELSE
    gotoState("Inactive")            ; ### STATE ###  (I)
    myF_InitInactive()
    RETURN    ; - STOP -
ENDIF
;---------------------
    IF self.IsDisabled()
        self.Enable()                    ; triggers OnLoad() immediately
        Debug.Trace("SM: OnUpdate() [Initializing] self has been enabled.. " +sMyID)
        Utility.Wait(0.1)
    ENDIF

int i = 20
    WHILE (i > 0)
        i = i - 1
        IF self.Is3DLoaded()
            myF_RFM(TRUE)
            i = 0
         ELSE
             Utility.Wait(0.1)                ; 20 * 0.1 = 2.0 sec
         ENDIF
     ENDWHILE

    gotoState("Restocking")            ; ### STATE ###  (R)
    myF_InitRestock()
ENFUNCTION


;---------------------------
FUNCTION myF_Fill_StringID()
;---------------------------
    objectReference mRef = sef.GetLinkedRef()
    IF ( mRef )
    ELSE
        mRef = self
    ENDIF

    int i = mRef.GetActorBase().GetSex() as Int
    sMyID = mRef.GetPositionX() as Int + "_" + mRef.GetPositionY() as Int + "_" + mRef.GetPositionZ() as Int + "_" + i
ENDFUNCTION


;-----------------
FUNCTION StartUp()  ; called by external script
;-----------------
    UnregisterForUpdate()
    gotoState("Initializing")        ; ### STATE ###  Init
    RegisterForSingleUpdate(1.0)
ENDFUNCTION


;================================
state Initializing
;=================
EVENT OnBeginState()
    Debug.Trace("SM: StoreMannequinActor: " + sMyID + ": OnBeginState: State: Initializing")
ENDEVENT

EVENT OnEndState()
    Debug.Trace("SM: StoreMannequinActor: " + sMyID + ": OnEndState: State: Initializing")
ENDEVENT

EVENT OnLoad()
ENDEVENT

EVENT OnUpdate()
IF ( bInitialized )
    RegisterForSingleUpdate(0.5)    ; keep updating until a working state can process or reject it
    RETURN    ; - STOP -
ENDIF
;---------------------
    bInitialized = TRUE
    self.EnableAI(False)                    ; switch off AI
    self.IgnoreFriendlyHits()

    IF self.GetBaseActorValue("CarryWeight") < 10000)
        self.SetActorValue("CarryWeight", 10000)
    ENDIF

    IF (sMyID == "")
        myF_Fill_StringID()
    ENDIF

    string s = "SM: OnUpdate() [Initializing] "

IF self.GetLinkedRef()
ELSE
    Debug.Trace(s+"no linkedRef found.. " +sMyID)
    shutDown()
    RETURN    ; - STOP -
ENDIF
;---------------------
IF ( SM_PairedMannequin )
ELSE
    Debug.Trace(s+"missing keyword 'SM_PairedMannequin' " +sMyID)
    shutDown()
    RETURN    ; - STOP -
ENDIF
;---------------------
    IF ( myPairedMannequin )
        myPairedMannequin = self.GetLinkedRef(SM_PairedMannequin) as foam_SM_StoreMannequinActor
    ENDIF

IF (myPairedMannequin == self)
    Debug.Trace(s+"myPairedMannequin is equal to self! " +sMyID)
    shutDown()
    RETURN    ; - STOP -
ENDIF
;---------------------
IF ( myPairedMannequin )
ELSE
    IF ( bDefaultGender )
        Debug.Trace(s+"paired mannequin not found! " +sMyID)
    ELSE
        Debug.Trace(s+"No paired mannequin on non-default mannequin! " +sMyID")
        shutDown()
        RETURN    ; - STOP -
    ENDIF
;    ----------------------
ENDIF

    IF (myPairedMannequin.GetLinkedRef(SM_PairedMannequin) == self)
    ELSE
        Debug.Trace(s+"myPairedMannequin doesn't pair back! " +sMyID)
    ENDIF

    IF ( bDefaultGender )
        IF myPairedMannequin.bDefaultGender
            Debug.Trace(s+"Mannequin and paired mannequin, defaultGender is TRUE! " +sMyID)        
        ENDIF
    ELSE
        IF myPairedMannequin.bDefaultGender
            bReverseEnableStatus  = myPairedMannequin.bReverseEnableStatus
            myEnableParent        = myPairedMannequin.myEnableParent
            CrimeFaction          = myPairedMannequin.CrimeFaction
            myOwningFaction       = myPairedMannequin.myOwningFaction
            iSaleRadius           = myPairedMannequin.iSaleRadius
            PossibleOwners        = myPairedMannequin.PossibleOwners
            bGenderUpdating       = myPairedMannequin.bGenderUpdating

            IF (OutfitList.Length == 0)
                OutfitList = myPairedMannequin.OutfitList
            ENDIF

            IF (QuestCompleteReqs.Length == 0)
                QuestCompleteReqs = myPairedMannequin.QuestCompleteReqs
            ENDIF

            IF (bPurchaseFlags.Length == 0)
                bPurchaseFlags = myPairedMannequin.bPurchaseFlags
            ENDIF

            IF (RequiredFactions.Length == 0)
                RequiredFactions = myPairedMannequin.RequiredFactions
            ENDIF

            IF (ExcludedFactions.Length == 0)
                ExcludedFactions = myPairedMannequin.ExcludedFactions
            ENDIF
        ELSE
            Debug.Trace(s+"Mannequin and paired mannequin, defaultGender is False! " +sMyID)
        ENDIF
    ENDIF

IF (iSaleRadius <= 0)
    Debug.Trace(s+"Invalid sale radius! " +sMyID)
    shutDown()
    RETURN    ; - STOP -
ENDIF
;---------------------
    IF (iSaleRadius < 400) || (iSaleRadius > 4000)
        Debug.Trace(s+"Sale radius is out of bounds! " +iSaleRadius+ "  " +sMyID)
    ENDIF

IF ( myOwningFaction )
ELSE
    Debug.Trace(s+"myOwningFaction is None! " +sMyID)
    shutDown()
    RETURN    ; - STOP -
ENDIF
;---------------------
IF myOwningFaction.IsFactionFlagSet(0x00004000)        ; kVendorFlag
ELSE
    Debug.Trace(s+"myOwningFaction has not the vendor flag! " +sMyID)
    shutDown()
    RETURN    ; - STOP -
ENDIF
;---------------------
    objectReference oRef = myOwningFaction.GetMerchantContainer()

IF ( oRef )                     ; oRef = myChest
ELSE
    Debug.Trace(s+"Merchant chest container does not exist! " +sMyID)
    shutDown()
    RETURN    ; - STOP -
ENDIF
;---------------------
    myChestAlias = SM_MerchantChestQuest.RegisterChest(oRef) as foam_SM_MerchantChestAlias

IF ( myChestAlias )
ELSE
    Debug.Trace(s+"No ChestAlias provided! " +sMyID)
    shutDown()
    RETURN    ; - STOP -
ENDIF
;---------------------
    IF ( CrimeFaction )
        self.SetCrimeFaction(CrimeFaction)
    ELSE
        Debug.Trace(s+"No crime faction! " +sMyID)
    ENDIF

    IF (OutfitList.Length == 0)
        Debug.Trace(s+"No Outfit provided! " +sMyID)
    ENDIF

    self.AddToFaction(SM_StoreMannequinFaction)
    SM_ControlQuest.IncrementMannequins()
    SM_ShutdownMannequinActorList.RemoveAddedForm(self)

    myF_InitGoto()
ENDEVENT
;=======
endState


;-------------------------
FUNCTION myF_InitRestock()
;-------------------------
    bUsePaired     = False
    bRestockNoWait = false

    self.BlockActivation(TRUE)
    self.RemoveAllItems()
;------------------------------------------ Adds mannequin's default display items
    ;AddDisplayItems()
    myOutfit     = GetOutfit()
    bPurchasable = GetPurchasable()
    self.SetFactionRank(SM_StoreMannequinFaction, bPurchasable as Int)

; Adds the items from the leveled list    
    IF ( myOutfit )
        self.AddItem(myOutfit)
    ENDIF

; Updates merchant container
    UpdateList()
;------------------------------------------
    EquipAll()

    bRestockQueued = False
    bQueueActive   = TRUE
    RegisterForSingleUpdate(0.1)
ENDFUNCTION


;================================
state Restocking
;===============
EVENT OnBeginState()
    Debug.Trace("SM: StoreMannequinActor: " + sMyID + ": OnBeginState: State: RESTOCKING")
ENDEVENT

EVENT OnEndState()
    Debug.Trace("SM: StoreMannequinActor: " + sMyID + ": OnEndState: State: RESTOCKING")
ENDEVENT

EVENT OnLoad()
    Debug.Trace("SM: StoreMannequinActor: " + sMyID + ": OnLoad: State: RESTOCKING")
    myF_RFM(TRUE)
ENDEVENT

EVENT OnUpdate()
IF ( bQueueActive )
    bQueueActive = False
    gotoState("Active")                ; ### STATE ###  (A)
    myF_InitActive()
    RETURN    ; - STOP -
ENDIF
;---------------------
    IF ( bRestockQueued )
        bRestockQueued = False
        bRestockNoWait = False
    ENDIF

    IF ( bGenderUpdateQueued )
        RegisterForSingleUpdate(0.5)
    ENDIF
ENDEVENT

EVENT OnUnload()
    Debug.Trace("SM: StoreMannequinActor: " + sMyID + ": OnUnLoad: State: RESTOCKING")

    UnregisterForLOS(Game.GetPlayer(), self as ObjectReference)
    myF_RFM(False)
ENDEVENT

EVENT OnGainLOS(Actor akViewer, ObjectReference akTarget)
    ;Debug.Trace("SM: StoreMannequinActor: " + sMyID + ": OnGainLOS: State: RESTOCKING")
    PopToMarker()
EndEvent

EVENT OnMenuClose(String asMenuName)    ; SKSE only
    ;Debug.Trace("SM: StoreMannequinActor: " + sMyID + ": OnMenuClose: State: RESTOCKING")
    RegisterForSingleLOSGain(Game.GetPlayer(), self as ObjectReference)
ENDEVENT
;=======
endState


;------------------------
FUNCTION myF_InitActive()
;------------------------
    IF self.IsDisabled()
        PopToMarker()
        self.Enable()                    ; trigger OnLoad() in same state
    ENDIF

    self.BlockActivation(False)
    PopToMarker()

    IF (bRestockQueued) || (bGenderUpdateQueued)
        UnRegisterForUpdate()
        RegisterForSingleUpdate(0.0)
    ENDIF
ENDFUNCTION


;================================
state Active
;===========
EVENT OnBeginState()
    Debug.Trace("SM: " +sMyID+ ": OnBeginState() [Active]")
ENDEVENT

EVENT OnEndState()
    Debug.Trace("SM: " +sMyID+ ": OnEndState() [Active]")
ENDEVENT

EVENT OnLoad()
    Debug.Trace("SM: StoreMannequinActor: " + sMyID + ": OnLoad: State: ACTIVE")
    myF_RFM(TRUE)
ENEVENT

EVENT OnUpdate()
    ;Debug.Trace("SM: StoreMannequinActor: " + sMyID + ": OnUpdate: State: ACTIVE")

IF ( bRestockQueued )
ELSE
    UpdateGender()                            ; ** 1 **
    RETURN    ; - STOP -
ENDIF
;---------------------
IF (bRestockNoWait) || !self.IsNearPlayer()
;;    Debug.Trace("SM: StoreMannequinActor: " + sMyID + ": OnUpdate: State: ACTIVE: Triggering restock")
    gotoState("Restocking")            ; ### STATE ###  (R)
    IF ( bGenderUpdateQueued )
        RegisterForSingleUpdate(0.5)
    ENDIF
    myF_InitRestock()
    RETURN    ; - STOP -
ENDIF
;---------------------
    ;Debug.Trace("SM: StoreMannequinActor: " + sMyID + ": OnUpdate: State: ACTIVE: Restock postponed, player near.")

IF ( bGenderUpdateQueued )
    UpdateGender()                            ; ** 2 **
    RETURN    ; - STOP -
ENDIF
;---------------------
     RegisterForSingleUpdate(2.0)        ; keep updating, but do not change the state
ENDEVENT

EVENT OnUnload()
    ;Debug.Trace("SM: StoreMannequinActor: " + sMyID + ": OnUnLoad: State: ACTIVE")

    UnregisterForLOS(Game.GetPlayer(), self as ObjectReference)
    myF_RFM(False)
ENDEVENT

EVENT OnGainLOS(Actor akViewer, ObjectReference akTarget)
    ;Debug.Trace("SM: StoreMannequinActor: " + sMyID + ": OnGainLOS: State: ACTIVE")
    PopToMarker()
ENDEVENT

EVENT OnMenuClose(string asMenuName)
    ;Debug.Trace("SM: StoreMannequinActor: " + sMyID + ": OnMenuClose: State: ACTIVE")
    RegisterForSingleLOSGain(Game.GetPlayer(), self as ObjectReference)
ENDEVENT

EVENT OnActivate(ObjectReference akActionRef)
; Forces mannequin to its marker and opens the inventory screen for item management
;;    Debug.Trace("SM: StoreMannequinActor: " + sMyID + ": OnActivate: State: ACTIVE")    

IF self.IsActivationBlocked()
    RETURN    ; - STOP -
ENDIF
;---------------------
IF ( bActivating )
    RETURN    ; - STOP -
ENDIF
;---------------------
    bActivating = TRUE        ; *T*
    PopToMarker()
    
IF (akActionRef == Game.GetPlayer() as ObjectReference)
    IF (GetOutfit(bUsePaired) == myOutfit)
        IF (GetPurchasable(bUsePaired) == bPurchasable)
        ELSE
            bPurchasable = !bPurchasable                                        ; Update list checks the purchase status, so set it first
            self.SetFactionRank(SM_StoreMannequinFaction, bPurchasable as Int)
            UpdateList()
        ENDIF
    ENDIF

    IF (akActionRef as Actor).IsSneaking()
    ELSE
        actor aRef = GetOwner()                                ; aRef = myOwner
        IF ( aRef )
            aRef.Say(BarterTopic)
        ELSE
            Debug.Notification("No merchant available")
        ENDIF
    ENDIF
ENDIF

    bActivating = False        ; ***
ENDEVENT


EVENT OnItemAdded(Form akBaseItem, Int aiItemCount, ObjectReference akItemReference, ObjectReference akSourceContainer)
IF ( akSourceContainer )
ELSE
    UpdateList()
    RETURN    ; - STOP -
ENDIF
;---------------------
    self.AddInventoryEventFilter(SM_EmptyFormList)
;    --------
    IF ( akItemReference )
        self.RemoveItem(akItemReference as Form, aiItemCount, TRUE, akSourceContainer)        ; trigger OnItemRemoved() in same state
    ELSE
        self.RemoveItem(akBaseItem, aiItemCount, TRUE, akSourceContainer)                    ; trigger OnItemRemoved() in same state
    ENDIF
;    -----------
    self.RemoveInventoryEventFilter(SM_EmptyFormList)
ENDEVENT


EVENT OnItemRemoved(Form akBaseItem, Int aiItemCount, ObjectReference akItemReference, ObjectReference akDestContainer)
;;    Debug.Trace("SM: StoreMannequinActor: " + sMyID + ":  Item: " + akBaseItem.GetName() + ": Removed")

    QueueNiNodeUpdate()
    UpdateList()
ENDEVENT
;=======
endState


;--------------------------
FUNCTION myF_InitInactive()
;--------------------------
    bRestockQueued = False
    bRestockNoWait = False

    UnregisterForLOS(Game.GetPlayer(), self as ObjectReference)
    myF_RFM(False)

    self.BlockActivation(TRUE)

    IF self.IsDisabled()
    ELSE
        PopToMarker()
        self.DisableNoWait()
     ENDIF

    UpdateList()
ENDFUNCTION


;================================
state Inactive
;=============
EVENT OnBeginState()
    Debug.Trace("SM: " +sMyID+ ": OnBeginState() [Inactive]")
ENDEVENT

EVENT OnEndState()
    Debug.Trace("SM: " +sMyID+ ": OnEndState() [Inactive]")
ENDEVENT

EVENT OnLoad()
ENDEVENT

EVENT OnUpdate()
;;    Debug.Trace("SM: StoreMannequinActor: " + sMyID + ": OnUpdate: State: INACTIVE: Restock queued: " + bRestockQueued + ": Gender update queued: " + bGenderUpdateQueued)
    IF ( bRestockQueued )
        bRestockQueued = False
        bRestockNoWait = False
    ENDIF

    IF ( bGenderUpdateQueued )
        bGenderUpdateQueued = False
        bGenderUpdating[bDefaultGender as Int] = False
        IF GetEnableTargetStatus()
            gotoState("Active")        ; ### STATE ###  (A)
            myF_InitActive()
        ENDIF
    ENDIF
ENDEVENT
;=======
endState


; -- FUNCTIONs -- 15

;-------------------------
FUNCTION myF_RFM(Bool bOK)  ; internal helper
;-------------------------
IF (SM_SKSEVersion.GetValue() == 0)
    RETURN    ; - STOP -
ENDIF
;---------------------
    IF ( bOK )
        RegisterForMenu("Sleep/Wait Menu")            ; sWaitMenu
        RegisterForMenu("MapMenu")                    ; sMapMenu
    ELSE
        UnregisterForMenu("Sleep/Wait Menu")
        UnregisterForMenu("MapMenu")
    ENDIF
ENDFUNCTION


;--------------------
FUNCTION UpdateList()
;--------------------
; sends mannequin inventory data to the merchant chest script
;;    Debug.Trace("SM: StoreMannequinActor: " + sMyID + ": UpdateList: Purchasable: " + bPurchasable)

IF ( myChestAlias )
ELSE
    RETURN    ; - STOP -    alias script is missing
ENDIF
;---------------------
    form[] a                                        ; a = myInventory

    IF ( bPurchasable )
        a = GetContainerForms()                        ; SKSE required !!!  // Returns all base forms from the container into a new array (ObjectReference.psc)
    ELSE
        a = Utility.CreateFormArray(0)                ; SKSE required !!!
    ENDIF

    myChestAlias.UpdateLists(a, self)
ENDFUNCTION


;-------------------------------------
Int FUNCTION CalcPriority(Form akForm)  ; internal helper
;-------------------------------------
; calculates the priority of equipping a given item

IF ( akForm )
ELSE
    RETURN 0                ; / 1    no priority
ENDIF
;---------
    int iPriority = 0
    armor AR = akForm as Armor                        ; AR = testArmor

IF ( AR )
    int iSlotMask= AR.GetSlotMask()                    ; SKSE required !!!
    int i = 0x01                                    ; i = iTestSlot
    WHILE (iT < AR.kSlotMask61)

        IF Math.LogicalAnd(i, iSlotMask)            ; SKSE required !!!
            IF     (i == AR.kSlotMask32)
                iPriority += 0x800
            ELSEIF (i == AR.kSlotMask30) || (i == AR.kSlotMask31) || (i == AR.kSlotMask41) || (i == AR.kSlotMask42) || (i == AR.kSlotMask43)
                ; head slots
                iPriority += 0x080
            ENDIF

                iPriority += 0x010        ; additional slot counter update
        ENDIF

        i = i * 2
    ENDWHILE
ENDIF

    i = akForm.GetGoldValueInt() / 10                ; gold value as tiebreaker
    IF (i > 0x080)
        i = 0x080
    ENDIF

    RETURN (iPriority + i)    ; /2    gold priority
ENDFUNCTION

;------------------
FUNCTION EquipAll()
;------------------
    form[] a                                                                ; a = myInventory
    a = GetContainerForms()                ; SKSE required !!!

    int j
    int i = a.Length                                                        ; iMax = iLength
    int iMax = i

IF (i > 1)
    int[] b = CreateIntArray(i)            ; SKSE required !!!                    ; b = iPriorityList

    WHILE (i > 0)
        i = i - 1
        b[i] = CalcPriority( a[i] )
    ENDWHILE

    i = 1                                    ; *** sort by insertion ***
    WHILE (i < iMax)
        int  T  = b[i]                                                        ; T = iTempPriority
        form fm = a[i]                                                        ; fm = TempForm
;        ----------------------------
        j = i - 1
        WHILE (j >= 0) && (b[j] > T)
            b[j+1] = b[j]                ; priority
            a[j+1] = a[j]                ; inventory
            j = j - 1
        ENDWHILE
        b[j+1] = T
        a[j+1] = fm
;        ----------------------------
        i = i + 1
    ENDWHILE
ENDIF
    
    bool bOK = self.GetActorBase().GetSex() as Bool                            ; bOK = myGender
    WHILE (i > 0)
        i = i - 1

        armor AR = a[i] as Armor                                            ; AR = testArmor
        IF ( AR )                                ; makes sure only armour is going to be equipped
            bool bFound = TRUE

            j = AR.GetNumArmorAddons()            ; makes sure only armour with visible models are equipped // SKSE required !!!
            WHILE (j > 0)
                j = j - 1
                ArmorAddon ArA = AR.GetNthArmorAddon(j)                        ; ArA = testAddon
                IF (ArA.GetModelPath(False, False) == "")                    ; SKSE required !!!
                    IF ( bOK )
                        IF (ArA.GetModelPath(False, TRUE) == "")
                            bFound = False
                        ENDIF
                    ELSE
                            bFound = False
                    ENDIF
                ENDIF
            ENDWHILE

            IF ( bFound )
                self.EquipItem(a[i], TRUE)        
            ENDIF
        ENDIF
    ENDWHILE
ENDFUNCTION


;----------------------------------------------
Actor FUNCTION GetOwner(Bool bFullCheck = TRUE)
;----------------------------------------------
;finds the mannequins owner(s) and, optionally, if they are available to sell merchandise.

IF ( myOwningFaction )
ELSE
    RETURN None        ; /1    no faction, no actor
ENDIF
;---------
    actor myOwner
    objectReference playerRef = Game.GetPlayer() as ObjectReference

int i = PossibleOwners.Length
WHILE (i > 0)
    i = i - 1
    actor aRef = PossibleOwners[i]
    IF (aRef) && !aRef.IsDead() && !aRef.IsDisabled() && (aRef.GetFactionRank(JobMerchantFaction) >= 0) && (aRef.GetFactionRank(myOwningFaction) >= 0)
        IF ( bFullCheck )
            IF aRef.IsActivationBlocked() || aRef.IsInCombat() || (aRef.GetDistance(playerRef) > iSaleRadius)
            ELSE
                SM_IsVendingFFSelf.Cast(aRef as ObjectReference, aRef as ObjectReference)
                SM_IsTalkingFFSelf.Cast(aRef as ObjectReference, aRef as ObjectReference)
                Utility.WaitMenuMode(0.05)

                IF SM_VendingActorList.HasForm(aRef) && !SM_TalkingActorList.HasForm(aRef)
                    myOwner = aRef
                ENDIF

                aRef.DispelSpell(SM_IsTalkingFFSelf)
                aRef.DispelSpell(SM_IsVendingFFSelf)
            ENDIF
        ELSE
            myOwner = aRef
        ENDIF
    ENDIF
ENDWHILE

    RETURN myOwner        ; /2
ENDFUNCTION


;----------------------
Int FUNCTION GetIndex()
;----------------------
;Loops through the list of quests
;The last completed quest (or blank value in the list) will be used as the index number
;to determine which leveled list is added and/or purchase flag used. If no list exists, the index is zero

    int iMax = QuestCompleteReqs.Length

IF (iMax == 0)
    RETURN 0
ENDIF
;---------
    int n = 0                                    ; n = outfit_index
    int i = 0
    WHILE (i < iMax)
        quest q = QuestCompleteReqs[i]
        IF (!q) || q.IsCompleted()
            n = i
        ENDIF
        i = i + 1
    ENDWHILE
    RETURN n
ENDFUNCTION


;-------------------------------------------------------
LeveledItem FUNCTION GetOutfit(Bool abUsePaired = False)
;-------------------------------------------------------
    foam_SM_StoreMannequinActor ps = self            ; ps = testMannequin
    IF ( abUsePaired )
        ps = myPairedMannequin
    ENDIF

    int i = ps.GetIndex()                            ; i = iQuestIndex

    leveledItem LI                                    ; LI = testOutfit
    IF (i < ps.OutfitList.Length)
        LI = ps.OutfitList[i]
    ENDIF
    
    RETURN LI
ENDFUNCTION


;----------------------------------------------------
Bool FUNCTION RestockInventory(Bool abNoWait = False)  ; called external only
;----------------------------------------------------
    ;Debug.Trace("SM: StoreMannequinActor: " + sMyID + ": Restock: Called")

IF (self.GetState() == "Active")
ELSE
    Return False    ; /1 state does not matching
ENDIF
;---------
IF GetEnableTargetStatus()
ELSE
    gotoState("Inactive")            ; ### STATE ###  (I)
    myF_InitInactive()
    Return False    ; /2
ENDIF
;---------
IF GetOwner(false)
ELSE
    Debug.Trace("SM: StoreMannequinActor: " + sMyID + ": Restock: WARNING: No owner found")
    Return False    ; /3
ENDIF
;---------
    Debug.Trace("SM: StoreMannequinActor: " + sMyID + ": Restock: Queued, will be registered again")

    bRestockQueued = TRUE
    bRestockNoWait = abNoWait
    RegisterForSingleUpdate(0.1)            ; be careful with this !
    Return TRUE        ; /4  well done!
ENDFUNCTION


;-----------------------------------------------------------------------
FUNCTION RemoveRegisteredItem(Form akRemoveForm = None, Int aiCount = 1)  ; called external only
;-----------------------------------------------------------------------
IF (akRemoveForm) && (self.GetState() == "Active")
ELSE
    RETURN    ; - STOP -    form is None /or/ state does not matching
ENDIF
;---------------------
    self.AddInventoryEventFilter(SM_EmptyFormList)
    self.RemoveItem(akRemoveForm, aiCount, TRUE)
    self.RemoveInventoryEventFilter(SM_EmptyFormList)
    self.QueueNiNodeUpdate()
ENDFUNCTION


;---------------------
FUNCTION PopToMarker()
;---------------------
IF ( bPopping )
    RETURN    ; - STOP -
ENDIF
;--------------------- moves the mannequin to its assigned XMarkerHeading
    bPopping = TRUE

    ;Debug.Trace("SM: StoreMannequinActor: " + sMyID + ": PopToMarker: Disabled: " + IsDisabled() + ": 3DLoaded: " + Is3DLoaded())
    self.MoveTo( self.GetLinkedRef() )            ; myMarker
    self.QueueNiNodeUpdate()

    bPopping = False
ENDFUNCTION


; ####################
; ### UpdateGender ###############################################################################
; ####################

;------------------------------------
Bool FUNCTION GetEnableTargetStatus()
;------------------------------------
IF (self.GetState() == "Error")
    Return False    ; /1
ENDIF
;---------
IF (!SM_MiscEnableMarker) || SM_MiscEnableMarker.IsDisabled()
    Return False    ; /2
ENDIF
;---------
    bool bOK = False                                    ; bOK = bGenderMatch

    IF ( myPairedMannequin )
        int i = SM_MannequinGenders.GetValueInt()        ; i = iMannequinGenders

        IF (i > 0)            ; 1,2
            bOK = (self.GetActorBase().GetSex() == (i - 1))    ; ((i == 1) && self.GetActorBase().GetSex() == 0) || ((i == 2) && self.GetActorBase().GetSex() == 1)
        ELSE
            i = SM_MannequinGenderOverrideActorList.Find(self as Form)
            IF ( bDefaultGender )
                bOK = (i < 0)                                ; (bDefaultGender && !bOverride)
            ELSE
                bOK = (i >= 0)    ; self found in formlist    ; (!bDefaultGender && bOverride)
            ENDIF
        ENDIF
    ELSE
        bOK = TRUE
    ENDIF

IF ( bOK )
ELSE
    Return False    ; /    3                                ; bGenderMatch == false
ENDIF
;---------

IF ( myEnableParent )
ELSE
    Return TRUE        ; / 4
ENDIF
;---------
IF (bReverseEnableStatus) && myEnableParent.IsDisabled()
    Return TRUE        ; / 5
ENDIF
;---------
IF (!bReverseEnableStatus) && !myEnableParent.IsDisabled()
    Return TRUE        ; /    6            
ENDIF
;---------
    Return False    ; / 7
ENDFUNCTION


;---------------------------
Bool FUNCTION UpdateGender()  ; state "Active" only
;---------------------------
;;    Debug.Trace("SM: StoreMannequinActor: " + sMyID + ": OnUpdate: State: ACTIVE: Updating gender")

IF ( bGenderUpdateQueued )            ; *T*
ELSE
    Return False
ENDIF
;---------
    bool bActive = GetEnableTargetStatus()
    bGenderUpdating[bDefaultGender as Int] = False

    int i = 0                                                ; i = iLoops
    WHILE (bGenderUpdating[0]) || (bGenderUpdating[1])
        i = i + 1
        Utility.WaitMenuMode(0.1)
    ENDWHILE

    bool bOK = (myPairedMannequin) && (!bActive)            ; bOK = bChanged

    IF (bOK) && myPairedMannequin.GetEnableTargetStatus()
        WHILE myPairedMannequin.IsDisabled()
            Utility.WaitMenuMode(0.1)
            Utility.Wait(0.1)
        ENDWHILE

        TransferItems()
        myPairedMannequin.UpdateList()
        UpdateList()
        myPairedMannequin.EquipAll()

        gotoState("Inactive")            ; ### STATE ###  (I)
        myF_InitInactive()
    ENDIF

    IF ( bRestockQueued )
        ;Debug.Trace("SM: StoreMannequinActor: " + sMyID + ": UpdateGender: Restock flag: " + bRestockQueued)
        RegisterForSingleUpdate(0.5)
    ENDIF

    bGenderUpdateQueued = False        ; ***
    RETURN bOK
ENDFUNCTION


;-----------------------
FUNCTION TransferItems()  ; internal helper // return Bool has been removed (ReDragon)
;-----------------------
IF ( myPairedMannequin )
ELSE
    Debug.Trace("SM: StoreMannequinActor: TransferItems: WARNING: No paired mannequin!")
;;;    Return False
    RETURN    ; - STOP -
ENDIF
;---------------------
IF self.IsDisabled()
    Debug.Trace("SM: StoreMannequinActor: " + sMyID + ": TransferItems: WARNING: Mannequin: " + sMyID + ": Disabled!")
;;;    Return False                ; missing !!!
    RETURN    ; - STOP -
ENDIF
;---------------------
IF myPairedMannequin.IsDisabled()
    Debug.Trace("SM: StoreMannequinActor:  " + sMyID + ": TransferItems: WARNING: Mannequin: " + myPairedMannequin.sMyID + ": Disabled!")        
;;;    Return False                ; missing !!!
    RETURN    ; - STOP -
ENDIF
;---------------------
; now copy all data to paired mannequin
    myPairedMannequin.bPurchasable = bPurchasable
    myPairedMannequin.myOutfit     = myOutfit
    myPairedMannequin.SetFactionRank(SM_StoreMannequinFaction, bPurchasable as Int)
    myPairedMannequin.bUsePaired = TRUE
    bUsePaired = False

; disables OnItemAdded/Removed events, transfers items, then re-enables the events
    myPairedMannequin.AddInventoryEventFilter(SM_EmptyFormList)
    self.AddInventoryEventFilter(SM_EmptyFormList)
    self.RemoveAllItems(myPairedMannequin, False, TRUE)                ; remove all items, be careful with this method !
    self.RemoveInventoryEventFilter(SM_EmptyFormList)
    myPairedMannequin.RemoveInventoryEventFilter(SM_EmptyFormList)

;;;    Return TRUE
ENDFUNCTION


;-----------------------------------------------------------
Bool FUNCTION QueueUpdateGender(Bool abChangeGender = False)  ; called external only
;-----------------------------------------------------------
WHILE (bGenderUpdateQueued)
    Utility.WaitMenuMode(0.1)
ENDWHILE

    bGenderUpdateQueued = TRUE
    UnRegisterForUpdate()

    bGenderUpdating[bDefaultGender as Int] = TRUE
    RegisterForSingleUpdate(0.1)

    Return TRUE
ENDFUNCTION


; ################
; ### Purchase ###################################################################################
; ################

;-----------------------------------------------------
Bool FUNCTION GetPurchasable(Bool abUsePaired = False)
;-----------------------------------------------------
    actor player = Game.GetPlayer()
    bool  bOK    = False
    faction fac

int i = ExcludedFactions.Length
    WHILE (i > 0) && (!bOK)
        i = i - 1
        fac = ExcludedFactions[i]
        IF (fac) && Player.IsInFaction(fac)
            bOK = TRUE
        ENDIF
    ENDWHILE

IF ( bOK )
    Return False    ; /1  player is part of excluded faction
ENDIF
;---------
    i = RequiredFactions.Length
    bOK = (i == 0)                                    ; empty array, player is included by default

    WHILE (i > 0) && (!bOK)
        i = i - 1
        fac = RequiredFactions[i]
        IF (fac) && Player.IsInFaction(fac)
            bOK = TRUE
        ENDIF
    ENDWHILE

IF ( bOK )
ELSE
    Return False    ; /2  player is not required
ENDIF
;---------
    foam_SM_StoreMannequinActor ps = self            ; ps = testMannequin
    IF ( abUsePaired )
        ps = myPairedMannequin
    ENDIF

    i = ps.GetIndex()
IF (i >= ps.bPurchaseFlags.Length)
    Return TRUE        ; /3
ENDIF
;---------
    bOK = ps.bPurchaseFlags[i]
    RETURN bOK        ; /4
ENDFUNCTION

 

 

Link to comment
Share on other sites

  • Recently Browsing   0 members

    • No registered users viewing this page.
×
×
  • Create New...