Jump to content

DOWNLOAD MODS

Are you looking for something shiny for your load order? We have many exclusive mods and resources you won't find anywhere else. Start your search now...

LEARN MODDING

Ready to try your hand at making your own mod creations? Visit the Enclave, the original ES/FO modding school, and learn the tricks of the trade from veteran modders...

JOIN THE ALLIANCE

Membership is free and registering unlocks image galleries, project hosting, live chat, unlimited downloads, & more...

[SKY] Dealing with stack dumps


BrettM
 Share

Recommended Posts

Before I entirely abandon my aquarium spawner, I would like to know what -- if anything -- can be done to keep all those little fish scripts from causing stack dumps. Each fish has a critter script to control its behavior, and even just eight of them running at once seems to overwhelm Papyrus, though not to the point of crashing me out of the game or even showing any lag. I have had as many as 28 going at once, in two different spawners, with no particular effect on gameplay but far more stack dumping.

 

I've posted the fish script below, in case I've done something foolish in it that is the root of the problem. Would it help if I dropped all the routines involved with schooling behavior? That processing is more complex, and I think it's resulting in jerkier motion than I might otherwise be able to achieve.

 

ScriptName fpiAquariumFish extends ObjectReference
{FPI Aquarium Kit: Behavior script for aquarium fish.}

import Utility
import Form
import Debug

; ---------------------------------------------------------------
; Script based on Critter and CritterFish scripts. Controls
; behavior of fish in a manner more suited to an aquarium, where
; fish do not need to flee the player and spawn/move in a volume
; unlike the usual CritterSpawn markers.

; Point-in-Polygon testing algorithms adapted to Papyrus from
; http://alienryderflex.com/polygon/
; ---------------------------------------------------------------

; ===============================================================
; Properties
; ===============================================================

; ---------------------------------------------------------------
; Properties set by spawner fpiAquariumFishSpawn
; ---------------------------------------------------------------

; The spawner that owns this fish
fpiAquariumFishSpawn Property Spawner auto hidden

; Maximum and minimum Z coordinates of movement volume.
Float Property fVolMaxZ auto hidden
Float Property fVolMinZ auto hidden

; Maximum and minimum X coordinates of movement volume.
Float Property fVolMaxX auto hidden
Float Property fVolMinX auto hidden

; Maximum and minimum Y coordinates of movement volume.
Float Property fVolMaxY auto hidden
Float Property fVolMinY auto hidden

; Number of corners defining plane transecting movement volume.
Int Property iCornerCount auto hidden

; Arrays of X and Y coordinates for the corners of the plane
Float[] Property fVolCornerX auto hidden
Float[] Property fVolCornerY auto hidden

; ---------------------------------------------------------------
; General properties
; ---------------------------------------------------------------

float Property fScaleMin = 0.10 auto
{Minimum scale of the fish: Default 0.10 (10%)}

float Property fScaleMax = 0.15 auto
{Maximum scale of the fish: Default 0.15 (15%)}

; ---------------------------------------------------------------
; Movement properties. Slower speeds are needed to cover the
; shorter aquarium distances without making movements look
; like a time-lapse film. Some curvature is needed to make
; movement more natural, but too much will result in a lot
; of paths that wander outside the boundaries of the volume.
; ---------------------------------------------------------------

Bool Property bMoving = false auto hidden

Float Property fTranslationSpeedMean = 10.0 auto
{The movement speed, mean value: Default 10.0}

Float Property fTranslationSpeedVariance = 2.0 auto
{The movement speed, variance: Default 2.0}

Float Property fSplineCurvature = 20.0 auto
{Curvature of spline travel path: Default 20.0}

; fMaxRotationSpeed original default was 360.0. According to
; the CK Wiki: "This is known to have issues. If you set
; afMaxRotationSpeed to anything higher than 0, it will
; not translate the rotation correctly. Other than
; *TranslateTo - ObjectReference, it will force the objects
; position at the destination point, when the translation ends.
; This may cause a sudden position change of objects."

; So we're going to hide this one since changing it would
; be pointless until there is a fix.

Float Property fMaxRotationSpeed = 0.0 auto hidden
{Max rotation speed while mocing: Default = 0.0 (No Clamp)}

; ---------------------------------------------------------------
; Schooling properties
; ---------------------------------------------------------------

Float Property fSchoolingDistanceX = 10.0 auto
{This fish stays within +/- X units of schooling partner: Default 10.0}

Float Property fSchoolingDistanceY = 10.0 auto
{This fish stays within +/- Y units of schooling partner: Default 10.0}

Float Property fSearchRadius = 30.0 auto
{Radius around this fish to search for a schooling partner: Default 30.0 units}

Int Property iPercentChanceSchooling = 50 auto
{Chance to school with another fish: Default 50%}

Int Property iPercentChanceStopSchooling = 5 auto
{Chance to stop schooling: Default 5%}

; ---------------------------------------------------------------
; Debugging
; ---------------------------------------------------------------

Bool Property bFishDebug = FALSE auto hidden

; ===============================================================
; Variables
; ===============================================================

; Would be used to prevent Die() more than once, except that
; we are unkillable. Kept in case we ever need killable
; aquarium fish for some bizarre reason.

Bool bKilled = false

; ---------------------------------------------------------------
; Movement and schooling. In either state we head for a target
; X/Y/Z and assume the target heading. The targets are set based
; on a chosen other fish if we are schooling. Else they are set
; randomly for the free-swimming state.
; ---------------------------------------------------------------

Float fTargetX
Float fTargetY
Float fTargetZ
Float fTargetAngleZ

Float fSpeed       ; Speed at which we will travel to the target

fpiAquariumFish TargetFish = none   ; Target school leader

; ---------------------------------------------------------------
; Point-in-Polygon variables
; ---------------------------------------------------------------

Float[]  fPipConstant   ; Precalculated constants
Float[]  fPipMultiple   ; Precalculated multipliers

; ---------------------------------------------------------------
; Startup-processing variables
; ---------------------------------------------------------------

Bool bDefaultPropertiesInitialized = false
Bool bSpawnerVariablesInitialized = false

; ---------------------------------------------------------------
; Receive incoming property values from fpiAquariumFishSpawn
; ---------------------------------------------------------------

; The spawner that owns this fish
fpiAquariumFishSpawn SpawnerPropVal

; Maximum and minimum Z coordinates of movement volume.
Float fMaxZPropVal
Float fMinZPropVal

; Maximum and minimum X coordinates of movement volume.
Float fMaxXPropVal
Float fMinXPropVal

; Maximum and minimum Y coordinates of movement volume.
Float fMaxYPropVal
Float fMinYPropVal

; Number of corners defining plane transecting movement volume.
Int iCornersPropVal

; Arrays of X and Y coordinates for the corners of the plane
Float[] fCornersXPropVal
Float[] fCornersYPropVal

; ===============================================================
; Events
; ===============================================================

; Stateless events  ;============================================

Event OnInit()

    ; Properties for this fish have been initialized. Default
    ; properties are now good.

    bDefaultPropertiesInitialized = true

    ; Allocate arrays for Point-in-Polygon pre-calculations.
    ; These must be the same size as fVolCornerX/Y arrays.

    fPipConstant = new Float[8]
    fPipMultiple = new Float[8]

    ; Go get the fish started if everything else is okay.

    CheckStateAndStart()

EndEvent

; ---------------------------------------------------------------

Event OnStart()

    ; Startup initialization, called after property initialization.

    ; Vary size a bit

    SetScale(RandomFloat(fScaleMin, fScaleMax))

    ; Start in the random swimming state

    GotoState("RandomSwimming")

    ; Place the fish and enable it

    WarpToRandomPoint()
    Enable()

    ; Wait for the 3d to be fully loaded then prepare to start swimming.

    int c = 0      ; Iteration counter
    int max = 10   ; Iteration limit

    while(!Is3DLoaded() && (c < max))
        wait(0.1)
        c += 1
    endwhile

    if (Is3DLoaded())
        SetMotionType(Motion_Keyframed, false) ; Switch to keyframe state
        RegisterForSingleUpdate(0.0)  ; Ask for an update to start moving
    else
        DisableAndDelete(false)  ; Suicide
    endif

EndEvent

; ---------------------------------------------------------------
; Deadly events. All short-circuited so we're unkillable by
; any known means other than suicide or detatchment.
; ---------------------------------------------------------------

; Event OnActivate(objectReference actronaut)
    ; Can't kill me this way. I can't be activated.
; EndEvent

Event OnHit(ObjectReference akAggressor, Form akWeapon, Projectile akProjectile, bool abPowerAttack, bool abSneakAttack, bool abBashAttack, bool abHitBlocked)
    ; Can't kill me this way. Weapons don't even tickle me.
;   Debug.Trace(self + " got hit by " + akAggressor)
EndEvent

Event OnMagicEffectApply(ObjectReference akCaster, MagicEffect akEffect)
    ; Can't kill me this way. I laugh at mages.
;   Debug.Trace(self + " got hit by magic of " + akCaster)
EndEvent

Event OnCellDetach()
    ; Safety measure - when my cell is detached, for whatever reason, kill me.
;   Debug.Trace("Killing self due to onCellDetach() - "+self)
    DisableAndDelete()
EndEvent

State RandomSwimming  ;==========================================

Event OnUpdate()

    ; Move at regular speed to next point.
    ; School if the dice roll that way and a companion is available.

    fSpeed = RandomFloat((fTranslationSpeedMean - fTranslationSpeedVariance), (fTranslationSpeedMean + fTranslationSpeedVariance))

    if ((RandomInt(0, 100) < iPercentChanceSchooling) && (PickTargetFishForSchooling()))
        GotoState("Schooling")
;       Debug.Trace(Self + " is changing to Schooling with target " + TargetFish)
        SchoolWithOtherFish(fspeed)  ; Follow our target
    else
;       Debug.Trace(Self + " is traveling alone at speed " + fSpeed)
        GoToNewPoint(fspeed) ; Go alone
    endIf

EndEvent

; ---------------------------------------------------------------

Event OnTranslationAlmostComplete()

    ; We have almost reached our target point. Register for an
    ; update so we can keep moving.

;   Debug.TraceConditional(Self + " almost at target", bFishDebug)
;   Debug.TraceConditional(Self + " registering for new update", bFishDebug)

    bMoving = false
    RegisterForSingleUpdate(0.0)

;   Debug.Trace(Self + " is now at X=" + fTargetX + ", Y=" + fTargetY + ", Z=" + fTargetZ)

EndEvent

EndState  ;======================================================

State Schooling  ;===============================================

Event OnUpdate()

    ; Move at regular speed to next point. Go alone if the dice
    ; say to stop schooling, or we lost our target, or our
    ; target is not presently moving. Otherwise keep following
    ; our leader.

    fSpeed = RandomFloat((fTranslationSpeedMean - fTranslationSpeedVariance), (fTranslationSpeedMean + fTranslationSpeedVariance))

    if ((RandomInt(0, 100) < iPercentChanceStopSchooling) || TargetFish == none || TargetFish.IsDisabled() || !TargetFish.bMoving)
        GotoState("RandomSwimming")
;       Debug.Trace(Self + " is changing to solitary travel at speed " + fSpeed)
        GoToNewPoint(fspeed) ; Go alone
    else
;       Debug.Trace(Self + " is Schooling with target " + TargetFish)
        SchoolWithOtherFish(fspeed) ; Follow our target
    endif

EndEvent

; ---------------------------------------------------------------

Event OnTranslationAlmostComplete()

    ; We have almost reached our target point. Register for an
    ; update so we can keep moving.

;   Debug.TraceConditional(Self + " is almost at target", bFishDebug)
;   Debug.TraceConditional(Self + " registering for new update", bFishDebug)

    bMoving = false
    RegisterForSingleUpdate(0.0)

;   Debug.Trace(Self + " is now at X=" + fTargetX + ", Y=" + fTargetY + ", Z=" + fTargetZ)

EndEvent

EndState  ;======================================================

; ===============================================================
; Movement and schooling functions
; ===============================================================

Function WarpToRandomPoint()

    ; Pick a random point for our first appearance when spawned.

    PickSpawnPoint()

    SetPosition(fTargetX, fTargetY, fTargetZ)
    SetAngle(0.0, 0.0, fTargetAngleZ)

;   Debug.Trace(Self.Spawner + " " + Self + " has been spawned at X=" + fTargetX + ", Y=" + fTargetY + ", Z=" + fTargetZ)

EndFunction

; ---------------------------------------------------------------

Function GoToNewPoint(Float afSpeed)

    ; Solitary movement: Pick a random point and travel to it.

    PickRandomPoint()
    bMoving = true
    SplineTranslateTo(fTargetX, fTargetY, fTargetZ, 0.0, 0.0, fTargetAngleZ, fSplineCurvature, afSpeed, fMaxRotationSpeed)

EndFunction

; ---------------------------------------------------------------

Function SchoolWithOtherFish(Float afSpeed)

    ; School movement: Follow a target other fish.

    if (TargetFish != none)
        PickRandomPointBehindTargetFish()
        bMoving = true
        SplineTranslateTo(fTargetX, fTargetY, fTargetZ, 0.0, 0.0, fTargetAngleZ, fSplineCurvature, afSpeed, fMaxRotationSpeed)
    else
;       Debug.Trace(Self + " tried to SchoolWithOtherFish, but no valid TargetFish.")
    endif

EndFunction

; ---------------------------------------------------------------

Function PickSpawnPoint()

    ; Pick a random point on a rectangular transect plane based
    ; on the min and max horizontal coordinates of the volume.
    ; Test to see if that point is also within a transect polygon
    ; within the box volume. If so, add a random height coord
    ; and heading to finish defining our destination.

    ; This is a version of PickRandomPoint used to place the
    ; fish when it first spawns. All fish start at the center
    ; of the tank and are warped to a new position, so we don't
    ; need the additional restrictions of PickRandomPoint that
    ; try to make movements look more natural.

    if !Spawner.GetParentCell()  ; Spawner must be unloaded.
        DisableAndDelete()       ; Suicide!!
    else
        int c = 0      ; Iteration counter
        int max = 10   ; Iteration limit

        Bool bPIP = false   ; Is the point in our polygon?

        while (!bPIP && (c < max))
            fTargetX = RandomFloat(fVolMinX, fVolMaxX)
            fTargetY = RandomFloat(fVolMinY, fVolMaxY)

            bPIP = PointInPolygon(fTargetX, fTargetY)

            c += 1
        endWhile

        ; If the loop was halted by exceeding the iteration
        ; limit and we still don't have a valid point, leave
        ; fish in the center. (Safety precaution not present
        ; in original script.)

        if !bPIP
            fTargetX = Spawner.X
            fTargetY = Spawner.Y
        endif

        ; Now we can pick a random height within the volume
        ; and a random target angle.

        fTargetZ = RandomFloat(fVolMinZ, fVolMaxZ)
        fTargetAngleZ = RandomFloat(-180.0, 180.0)
    endif

EndFunction

; ---------------------------------------------------------------

Function PickRandomPoint()

    ; Pick a random point on a rectangular transect plane based
    ; on the min and max horizontal coordinates of the volume.
    ; Test to see if that point is also within a transect polygon
    ; within the box volume. If so, add a random height coord
    ; and heading to finish defining our destination.

    ; Longer traversals result in more time spent moving and
    ; less appearance of a stop-and-go rush hour. We will
    ; accept a point only if it is at least 30 units away
    ; along either the X or Y axes.

    Float fDistanceX = 0.0   ; Abs X distance from current X
    Float fDistanceY = 0.0   ; Abs Y distance from current Y

    if !Spawner.GetParentCell()  ; Spawner must be unloaded.
        DisableAndDelete()       ; Suicide!!
    else
        int c = 0      ; Iteration counter
        int max = 10   ; Iteration limit

        Bool bPIP = false   ; Is the point in our polygon?

        while (!bPIP && (c < max))
            fTargetX = RandomFloat(fVolMinX, fVolMaxX)
            fDistanceX = Math.Abs(Self.X - fTargetX)

            fTargetY = RandomFloat(fVolMinY, fVolMaxY)
            fDistanceY = Math.Abs(Self.Y - fTargetY)

            if (fDistanceX >= 30.0) || (fDistanceY >= 30.0)
                bPIP = PointInPolygon(fTargetX, fTargetY)
            else
                ; Skip test. We don't want this point.
            endif

            c += 1
        endWhile

        ; If the loop was halted by exceeding the iteration
        ; limit and we still don't have a valid point, head
        ; for the center. (Safety precaution not present in
        ; original script.)

        if !bPIP
            fTargetX = Spawner.X
            fTargetY = Spawner.Y
        endif

        ; Now we need a Z coordinate for our destination. We
        ; want to limit plane changes to more modest values
        ; to prevent crash dives and zoom climbs. Combined
        ; with the 30-unit minimum translation distance, the
        ; chosen limit will result in a max plane-change
        ; angle of about 40 deg. Min/Max functions ensure
        ; the new value is inside the volume.

        fTargetZ = RandomFloat(MaxOf((Self.Z - 25.0), fVolMinZ), MinOf((Self.Z + 25.0), fVolMaxZ))

        ; Pick a random angle. The translate function is not
        ; making the fish follow a path that assumes this
        ; angle gradually, but is changing the fish abruptly
        ; to the target angle on arrival. I haven't figured
        ; out yet how to limit the change in angle in a way
        ; that at least prevents big about-face movements.

        fTargetAngleZ = RandomFloat(-180.0, 180.0)

    endif

EndFunction

; ---------------------------------------------------------------

bool Function PickTargetFishForSchooling()

    ; Pick a random fish other than ourselves from the list used
    ; by our spawner.

    ; Unlike the CritterFish script, we're going to search within
    ; a short radius centered on ourselves rather than on the
    ; spawner that owns us. We want to school with fish that are
    ; relatively near, not those on the other side of the tank.

    ; If someone sets up multiple fpiAquariumFishSpawn in close
    ; proximity, the radius may overlap with another spawner
    ; using the same FishType list. We need to be sure the
    ; suggested partner is owned by our own spawner.

    if Spawner.is3Dloaded()
        TargetFish = Game.FindRandomReferenceOfAnyTypeInListFromRef(Spawner.FishTypes, Self, fSearchRadius) as fpiAquariumFish
        return (TargetFish != none && TargetFish != self && TargetFish.Spawner == Self.Spawner)
    else
        return false
    endif

EndFunction

; ---------------------------------------------------------------

Function PickRandomPointBehindTargetFish()

    ; For an aquarium fish, we want to pick a closer following
    ; point than used for regular pond spawners, since space
    ; is limited and the fish are much smaller.

    if !TargetFish
;      Debug.Trace(Self + "tried to PickRandomPointBehindTargetFish, but no valid TargetFish.")
    else
        int c = 0      ; Iteration counter
        int max = 10   ; Iteration limit

        Bool bPIP = false    ; Is the point in our polygon?

        while (!bPIP && (c < max))

            ; Pick a random offset that will fall within
            ; schooling distance of a point and will be
            ; behind that point.

            Float fLocalDeltaX = RandomFloat(-fSchoolingDistanceX, fSchoolingDistanceX)
            Float fLocaldeltaY = RandomFloat(-fSchoolingDistanceY, 0.0)  ; Behind

            ; Rotate the offset to match the rotation of the target.

            Float fTargetFishHeading = TargetFish.GetAngleZ()
            Float fDeltaX = (Math.cos(fTargetFishHeading) * fLocalDeltaX) + (Math.sin(fTargetFishHeading) * fLocalDeltaY)
            Float fDeltaY = (Math.cos(fTargetFishHeading) * fLocalDeltaY) - (Math.sin(fTargetFishHeading) * fLocalDeltaX)

            ; Apply it to the position of the target fish to
            ; get absolute coordinates for our destination.
            ; Test the point to be sure it falls within the
            ; spawner's movement volume.

            fTargetX = TargetFish.X + fDeltaX
            fTargetY = TargetFish.Y + fDeltaY

            bPIP = PointInPolygon(fTargetX, fTargetY)

            c += 1
        endWhile

        ; If the loop was halted by exceeding the iteration
        ; limit and we still don't have a valid point, head
        ; for the center. (Safety precaution not present in
        ; original script.)

        if !bPIP
            fTargetX = Spawner.X
            fTargetY = Spawner.Y
        endif

        ; Now we need a Z coordinate for our destination. We
        ; want to limit plane changes to more modest values
        ; to prevent crash dives and zoom climbs. The target
        ; fish will generally be relatively close, so we want
        ; a much-lower limit than the one used for solitary
        ; travel destinations. It doesn't seem worth the extra
        ; calculations to derive an appropriate value based on
        ; the actual distance to the target fish. Min/Max
        ; functions ensure the new value is inside the volume.

        fTargetZ = RandomFloat(MaxOf((Self.Z - 5.0), fVolMinZ), MinOf((Self.Z + 5.0), fVolMaxZ))

        ; Point ourselves in the same direction as our
        ; buddy on arrival at destination.

        fTargetAngleZ = TargetFish.GetAngleZ()
    endif

EndFunction

; ===============================================================
; Startup functions
; ===============================================================

Function SetInitialSpawnerProperties(fpiAquariumFishSpawn arSpawner, Float afMaxZ, Float afMinZ, Float afMaxX, Float afMinX, Float afMaxY, Float afMinY, Int aiCorners, Float[] afCornersX, Float[] afCornersY)

    ; Called by spawner to pass properties once defaults have been set.
    ; Save the properties, indicate that they are init, and go do startup.

    SpawnerPropVal = arSpawner       ; The spawner that owns this fish

    fMaxZPropVal = afMaxZ            ; Maximum Z coord of movement volume
    fMinZPropVal = afMinZ            ; Minimum Z coord of movement volume

    fMaxXPropVal = afMaxX            ; Maximum X coord of movement volume
    fMinXPropVal = afMinX            ; Minimum X coord of movement volume

    fMaxYPropVal = afMaxY            ; Maximum Y coord of movement volume
    fMinYPropVal = afMinY            ; Minimum Y coord of movement volume

    iCornersPropVal = aiCorners      ; Number of corners of movement volume

    fCornersXPropVal = afCornersX    ; Array of corner X coords
    fCornersYPropVal = afCornersY    ; Array of corner Y coords

    bSpawnerVariablesInitialized = true

    CheckStateAndStart()

EndFunction

; ---------------------------------------------------------------

Function CheckStateAndStart()

    ; If everything is initialized, copy the saved spawner property
    ; values into the properties and kick off the behavior. Behavior
    ; startup is done asynchronously so that the spawner can get to
    ; work on the next fish.

;   Debug.TraceConditional(Self + "bDefaultPropertiesInitialized=" + bDefaultPropertiesInitialized + ", bSpawnerVariablesInitialized=" + bSpawnerVariablesInitialized, bFishDebug)

    if (bDefaultPropertiesInitialized && bSpawnerVariablesInitialized)
        SetSpawnerProperties()

        GotoState("KickOffOnStart")
        RegisterForSingleUpdate(0.0)
    endif

EndFunction

; ---------------------------------------------------------------

Function SetSpawnerProperties()

    ; Copy the incoming spawner values saved by
    ; SetInitialSpawnerProperties into the corresponding
    ; properties. Precalculate the constants and multipliers
    ; for the Point-In-Polygon tester using these values.

    Spawner = SpawnerPropVal         ; The spawner that owns this fish

    fVolMaxZ = fMaxZPropVal          ; Maximum Z coord of movement volume
    fVolMinZ = fMinZPropVal          ; Minimum Z coord of movement volume

    fVolMaxX = fMaxXPropVal          ; Maximum X coord of movement volume
    fVolMinX = fMinXPropVal          ; Minimum X coord of movement volume

    fVolMaxY = fMaxYPropVal          ; Maximum Y coord of movement volume
    fVolMinY = fMinYPropVal          ; Minimum Y coord of movement volume

    iCornerCount = iCornersPropVal   ; Number of corners of movement volume

    fVolCornerX = fCornersXPropVal   ; Array of corner X coords
    fVolCornerY = fCornersYPropVal   ; Array of corner Y coords

    PipPrecalc()                     ; Precalculate PIP constants/multipliers

EndFunction

; ---------------------------------------------------------------

State KickOffOnStart

Function OnUpdate()

    ; Control was passed to us via a RegisterForSingleUpdate,
    ; which made us asynchronous with the spawner, allowing
    ; it to get back to work. Nullify the state that got us
    ; here and call our own OnStart event to complete our
    ; initialization.

    GotoState("")

;   Debug.TraceConditional(Self + " is triggering OnStart", bFishDebug)

    OnStart()
    Enable()

EndFunction

EndState

; ===============================================================
; Death functions
; ===============================================================

Function Die()
    ; Nuh-uh! Can't kill a sweet, harmless aquarium fish.
    ; If I must go, then I will call DisableAndDelete() myself.
EndFunction

; ---------------------------------------------------------------

Function DisableAndDelete(bool abFadeOut = true)

    ; Unregister for any kind of update

    UnregisterForUpdate()
    UnregisterForUpdateGameTime()

    ; Disable the fish - don't wait if we aren't fading

    if !abFadeOut
        DisableNoWait()
    else
        Disable(abFadeOut)
    endif

    ; Stop any movement and delete ourselves

    if (GetParentCell()) ; Cell I'm in is still loaded
        StopTranslation()
        Delete()
    endif

    ; Notify spawner

    if (Spawner != none)
        Spawner.OnFishDied()
    endif

;   Debug.Trace(Self + " just killed itself.")

EndFunction

; ===============================================================
; Utility functions
; ===============================================================

float Function MaxOf(Float afValue1, Float afValue2)

    ; Return the greater of the two arguments. Arbitrarily
    ; return the second value if they are equal.

    Float fMaxVal

    if afValue1 > afValue2
        fMaxVal = afValue1
    else
        fMaxVal = afValue2
    endif

    return fMaxVal

EndFunction

; ---------------------------------------------------------------

float Function MinOf(Float afValue1, Float afValue2)

    ; Return the lesser of the two arguments. Arbitrarily
    ; return the second value if they are equal.

    Float fMinVal

    if afValue1 < afValue2
        fMinVal = afValue1
    else
        fMinVal = afValue2
    endif

    return fMinVal

EndFunction

; ===============================================================
; Point-in-Polygon test functions
; ===============================================================

; ---------------------------------------------------------------
;  Variables that must be set before calling these functions:
;
;  Int      iCornerCount   =  how many corners the polygon has
;  Float[]  fVolCornerX    =  horizontal coordinates of corners
;  Float[]  fVolCornerY    =  vertical coordinates of corners
;
;  Arrays that must be allocated at the same size as the above
;  arrays before calling these functions:
;
;  Float[]  fPipConstant   = precalculated constants
;  Float[]  fPipMultiple   = precalculated multipliers
;
;  USAGE:
;
;  Call PipPrecalc() to initialize the fPipConstant[] and
;  fPipMultiple[] arrays, then call PointInPolygon(x, y) to
;  determine if the point is in the polygon.
;
;  The function will return TRUE if the point x,y is inside the
;  polygon, or FALSE if it is not. If the point is exactly on
;  the edge of the polygon, then the function may return TRUE or
;  FALSE. This may cause fish to clip the container if adequate
;  clearance is not provided around the spawner.
;
;  Note that division by zero is avoided because the division
;  is protected by the "if" clause which surrounds it.
; ---------------------------------------------------------------

Function PipPrecalc()

    ; Follow the perimeter of the polygon, comparing the Y coords
    ; of each pair of adjacent corners. Set the constant and
    ; multiplier depending on whether the two corners share the
    ; same Y or are vertically separated.

    Int i = 0
    Int j = iCornerCount - 1

    while (i < iCornerCount)
        if (fVolCornerY[j] == fVolCornerY[i])
            fPipConstant[i] = fVolCornerX[i]
            fPipMultiple[i] = 0.0
        else
            fPipConstant[i] = fVolCornerX[i] - (fVolCornerY[i] * fVolCornerX[j]) / (fVolCornerY[j] - fVolCornerY[i]) + (fVolCornerY[i] * fVolCornerX[i]) / (fVolCornerY[j] - fVolCornerY[i])
            fPipMultiple[i] = (fVolCornerX[j] - fVolCornerX[i]) / (fVolCornerY[j] - fVolCornerY[i])
        endif

        j = i
        i += 1
    endwhile

EndFunction

; ---------------------------------------------------------------

bool Function PointInPolygon(Float afX, Float afY)

    ; Follow the perimeter of the polygon. For each pair of
    ; adjacent corners, determine if the side between them
    ; counts as a "node" with respect to the point being tested.

    ; If so, toggle the "OddNodes" boolean to denote whether we
    ; have discovered an even number or an odd number of nodes
    ; so far. If the final result is odd, then the point lies
    ; within the polygon.

    ; Note that if Papyrus ever impliments an XOR operator, the
    ; inner IF statement could be eliminated and the following
    ; statement replaced by:

    ;  OddNodes ^= ((afy * fPipMultiple[i] + fPipConstant[i]) < afX)

    Bool OddNodes = false

    Int i = 0
    Int j = iCornerCount - 1

    while (i < iCornerCount)
        if ((fVolCornerY[i] < afY && fVolCornerY[j] >= afY) || (fVolCornerY[j] < afY && fVolCornerY[i] >= afY))
            if ((afy * fPipMultiple[i] + fPipConstant[i]) < afX)
                OddNodes = !OddNodes
            endif
        endif

        j = i
        i += 1
    endwhile

    return OddNodes

EndFunction

 

I've also posted the spawner script, though that script is inactive once the fish have been spawned and does not register for any updates. The dumps don't begin until after all the fish have been spawned and have been moving around for a bit.

 

ScriptName fpiAquariumFishSpawn extends ObjectReference
{FPI Aquarium Kit: Spawner for aquarium fish.}

import fpiAquariumFish
import Utility

; ---------------------------------------------------------------
; Script based on CritterSpawn script. Spawns fish in one of the
; aquaria from the Aquarium Kit resource, or in any simple box
; marker extruded from any plane with 4-8 sides. Parameters
; and functions related to critters other than fish have been
; removed. The parameters defining the spawn/movement volume
; are greatly changed.

; The standard pond spawner is a cone pointing upward that was
; then Y-rotated -180 so the Z value is a reference to the top
; rather than the bottom of the volume. It is also R-rotated
; -180. Critter spawners for insects are spherical, except for
; dragonflies used with pond spawners.

; The Aquarium Kit spawner is the aquarium itself, which is a
; box with beveled corners. Box volumes require different means
; of defining the volume in which critters should spawn than the
; spherical or conical volumes used by standard critter markers.
; ---------------------------------------------------------------

; ---------------------------------------------------------------
; General properties
; ---------------------------------------------------------------

; Properties for the type and number of fish. The list for a
; Kit aquarium will usually be filled with FPI custom aquarium
; fishes, but pond fish or salmon may be prefered for aquaria
; scaled to a very large size. Box spawners used for other
; containers may use whatever type of list is appropriate for
; the context.

; All fishes in the list(s) used, however, must have a custom
; form using the fpiAquariumFish script rather than any of the
; standard critter scripts. E.g., to create a salmon list, first
; copy the standard Salmon01 and Salmon02 forms under new names,
; such as AquariumSalmon01/02, remove the CritterFish script
; from those copies, and add the fpiAquariumFish script. Then
; create a new FormList using the new salmon forms.

FormList Property FishTypes auto
{The base object listing the fishes that may spawn}

Int Property iMaxFishCount = 8 auto
{The number of fish to place in the aquarium: Default 8}

; Times during which the spawner may be active.
; Defaults to around the clock in game time.

GlobalVariable Property GameHour auto
{Make this point to the GameHour global (autofill)}

Float Property fSpawnTimeStart = 0.1 auto
{The Time after which this spawner can be active: Default 0.1}

Float Property fSpawnTimeEnd = 23.9 auto
{The Time before which this spawner can be active: Default 23.9}

; ---------------------------------------------------------------
; Properties defining the spawning/movement volume.

; Kit volume is an imaginary box following the X/Y shape of the
; tank, but inset from the walls of the actual tank. Z boundaries
; are determined by the pre-installed water plane towards
; the top of the tank and a plane slightly above the approximate
; mean height of the gravel terrain at the bottom.

; Volume for box spawners is simply the shape of the marker,
; which should be a box extruded from a plane with 4-8 sides.
; The height and depth are both established by the Z offset of
; the top of the box, which is assumed to be sized to give some
; clearance from the walls of the container in which it will be
; used and will be placed so the top is no higher than the water
; plane used in the container.

; This setup makes it simple to determine a Z coordinate, since
; the height is constant throughout the volume. For the X and Y
; coordinates, the point-selection algorithm needs the X/Y
; offsets of each of the corner vertices of a plane transecting
; the box to determine if a point falls within the volume.

; Kit spawners require coordinates for eight corners. Other box
; spawners may have as few as four, for simple rectangular boxes.
; Corners are defined starting with the front left (as seen
; looking in the +Y direction with the +X direction on the right)
; and proceeding clockwise around the perimeter.

; Defaults are set for a Kit aquarium of standard size. If
; the aquarium is scaled, all offset values except fDepthMin
; must be scaled by the same amount or the fish may spawn outside
; of the tank or will not properly fill the actual volume of the
; tank. The script will do all the calculations if the scale
; value applied to the aquarium is entered as the fVolumeScale
; parameter.
; ---------------------------------------------------------------

Float Property fVolumeScale = 1.0 auto
{Scaling factor applied to spawner: Default 1.0 (100%)}

Float Property fWaterHeight = 38.0 auto
{Height offset of the water plane or box top: Default 38.0}

Float Property fDepthMin = 2.0 auto
{Fish must stay at least this far below fWaterHeight: Default 2.0}

Float Property fDepthMax = 25.0 auto
{Fish may go no further than this below fWaterHeight: Default 25.0}

Int Property iCornerCount = 8 auto
{Number of corners defining spawner transect plane: Default 8 (4-8 valid)}

Float Property fCorner1X = -37.0 auto
{X offset of corner 1}
Float Property fCorner1Y = -19.0 auto
{Y offset of corner 1}

Float Property fCorner2X = -42.0 auto
{X offset of corner 2}
Float Property fCorner2Y = -12.0 auto
{Y offset of corner 2}

Float Property fCorner3X = -42.0 auto
{X offset of corner 3}
Float Property fCorner3Y = 12.0 auto
{Y offset of corner 3}

Float Property fCorner4X = -37.0 auto
{X offset of corner 4}
Float Property fCorner4Y = 19.0 auto
{Y offset of corner 4}

Float Property fCorner5X = 37.0 auto
{X offset of corner 5}
Float Property fCorner5Y = 19.0 auto
{Y offset of corner 5}

Float Property fCorner6X = 42.0 auto
{X offset of corner 6}
Float Property fCorner6Y = 12.0 auto
{Y offset of corner 6}

Float Property fCorner7X = 42.0 auto
{X offset of corner 7}
Float Property fCorner7Y = -12.0 auto
{Y offset of corner 7}

Float Property fCorner8X = 37.0 auto
{X offset of corner 8}
Float Property fCorner8Y = -19.0 auto
{Y offset of corner 8}

; ---------------------------------------------------------------
; Variables to keep track of spawned fish
; ---------------------------------------------------------------

Int Property iCurrentFishCount = 0 auto hidden
Bool bLooping
Bool bPrintDebug = FALSE     ; Should usually be set to false.
Int Recursions

; ---------------------------------------------------------------
; Coordinates that will be passed to spawned fish.
; ---------------------------------------------------------------

Float[] fVolCornerX ; Array of corner X coordinates
Float[] fVolCornerY ; Array of corner Y coordinates

Float   fVolMaxZ    ; Highest Z coordinate
Float   fVolMinZ    ; Lowest Z coordinate

Float   fVolMaxX    ; Highest X coordinate
Float   fVolMinX    ; Lowest X coordinate

Float   fVolMaxY    ; Highest Y coordinate
Float   fVolMinY    ; Lowest Y coordinate

; ---------------------------------------------------------------
; Misc
; ---------------------------------------------------------------

Float fRecheckWaitTime = 2.0 ; Wait time between checks of conditions

; ===============================================================
; Events
; ===============================================================

Event OnInit()

    ; Create arrays for coordinates. These will be filled after
    ; we are sure the 3d is loaded.

    fVolCornerX = New Float[8]
    fVolCornerY = New Float[8]

EndEvent

; ---------------------------------------------------------------

Event OnLoad()

    ; Initialize when the 3d is loaded. Loop until the conditions
    ; are met for spawning a batch of fish. Note that if the fish
    ; are somehow killed, no more will spawn until the cell is
    ; again reloaded.

    if bPrintDebug == TRUE
;       Debug.Trace(Self + " loaded.")
        recursions = 0
    endif

    bLooping = ValidProperties() ; No use looping if property values will cause misbehavior

    while bLooping == TRUE
        if bPrintDebug == TRUE
            Recursions += 1
;           Debug.Trace("Spawner " + Self +  " while loop #" + Recursions)
        endif

        if ShouldSpawn() == FALSE ; wait a bit and check conditions again
;           Debug.TraceConditional("Spawning conditions not yet met " + Self, bPrintDebug)
            utility.wait(fRecheckWaitTime)
        else
;           Debug.TraceConditional(Self + " ready to spawn!!!", bPrintDebug)
            SetVolumeCoordinates()
            SpawnInitialFishBatch()
            bLooping = FALSE ; Stop spawning until next load of the cell
        endif
    endwhile

EndEvent

; ---------------------------------------------------------------

Event OnFishDied()

    ; Called when a fish dies or is removed for any reason.
    ; Decrement the current fish count so we know to spawn a
    ; new one the next time OnUpdate is called.

    if iCurrentFishCount > 0
        iCurrentFishCount -= 1
    elseif iCurrentFishCount == 0
        ; Do nothing, since we shouldn't have a negative count.
    else ; Somehow the count went negative, so start working our way back up to zero.
        iCurrentFishCount += 1
    endif

;   Debug.Trace(Self + " just removed a dead fish")

EndEvent

; ---------------------------------------------------------------

Event OnUnload()
    ; when our 3D unloads, stop looping until loaded again.
    bLooping = FALSE
;   Debug.TraceConditional("Spawner " + Self + " unloading due to onUnload() EVENT.", bPrintDebug)
EndEvent

; ---------------------------------------------------------------

Event OnCellDetach()
    bLooping = FALSE
;   Debug.TraceConditional("Spawner " + Self + " unloading due to onCellDetach() EVENT.", bPrintDebug)
EndEvent

; ===============================================================
; Functions
; ===============================================================

bool Function ValidProperties()

    ; Check selected properties to see if the values make sense.
    ; Some will produce only harmless errors, such as a value <=0
    ; for iMaxFishCount. Others may produce some strange results
    ; or script blowups, such as a count of corner vertices that
    ; is not four or eight. If there is a problem, stop spawning
    ; before it starts.

    bool bPropsOK = true

    if (iMaxFishCount <= 0)
        bPropsOK = false
;       Debug.Trace(Self + " spawning prevented: iMaxFishCount is " + iMaxFishCount)
    endif

    if (fVolumeScale <= 0.0)
        bPropsOK = false
;       Debug.Trace(Self + " spawning prevented: fVolumeScale is " + fVolumeScale)
    endif

    if (fWaterHeight <= 0.0) || (fDepthMax <= 0.0) || (fDepthMax > fWaterHeight) || (fDepthMin <= 0.0) || (fDepthMin >= fDepthMax)
        bPropsOK = false
;       Debug.Trace(Self + " spawning prevented: Invalid Z volume boundaries")
    endif

    if (iCornerCount < 4) || (iCornerCount > 8)
        bPropsOK = false
;       Debug.Trace(Self + " spawning prevented: iCornerCount is " + iCornerCount)
    endif

    return bPropsOK

EndFunction

; ---------------------------------------------------------------

bool Function ShouldSpawn()

    ; Determine if all criteria are met for the spawner to start spawning.
    ; Check of player distance was removed. The 3d must be loaded and the time
    ; must be within the active time frame set by parameters.

    ; ===== Original Bethesda comments
    ; DREW - Added an extra safety measure for if the object is stuck in the spawn check loop while the 3d is not loaded
    ; NOTE - is3dLoaded dumps an error when the 3d is not loaded, but the function still returns false which should
    ; set bLooping to false and jump out of the bLooping While, at which point no additional errors will be thrown

    if Self.is3dLoaded()
        return IsActiveTime()
    else    ;if the 3d is not loaded jump out of the looped state. Just an extra safety measure.
;       Debug.Trace(Self + ": should be setting bLooping to False")
        bLooping = FALSE
        return false
    endif

EndFunction

; ---------------------------------------------------------------

bool Function IsActiveTime()

    ; Removed tests of weather conditions, since precipitation is irrelevant
    ; to aquarium fish.

    bool binTimeRange = false

    if (fSpawnTimeEnd >= fSpawnTimeStart)
        binTimeRange = (GameHour.GetValue() >= fSpawnTimeStart) && (GameHour.GetValue() < fSpawnTimeEnd)
    else
        binTimeRange = (GameHour.GetValue() >= fSpawnTimeStart) || (GameHour.GetValue() < fSpawnTimeEnd)
    endif

    return binTimeRange

EndFunction

; ---------------------------------------------------------------

Function SetVolumeCoordinates()

    ; Scale offsets if necessary and convert them to coordinates
    ; relative to the actual position of the spawner. The corner
    ; offsets must be adjusted for any rotation applied.

    fVolCornerX = New Float[8]    ; Init array of X coordinates
    fVolCornerY = New Float[8]    ; Init array of Y coordinates

    Float fAngleZ = Self.GetAngleZ() ; Angle for corner rotation

    ; Scale Z axis

    fVolMaxZ = ((fWaterHeight * fVolumeScale) - fDepthMin) + Self.Z
    fVolMinZ = ((fWaterHeight - fDepthMax) * fVolumeScale) + Self.Z

    ; Scale and adjust corner coordinates, placing the results in
    ; the arrays. We do all eight for simplicity, even if fewer
    ; will actually be used.

    PrepCorner(0, fCorner1X, fCorner1Y, fAngleZ)
    PrepCorner(1, fCorner2X, fCorner2Y, fAngleZ)
    PrepCorner(2, fCorner3X, fCorner3Y, fAngleZ)
    PrepCorner(3, fCorner4X, fCorner4Y, fAngleZ)
    PrepCorner(4, fCorner5X, fCorner5Y, fAngleZ)
    PrepCorner(5, fCorner6X, fCorner6Y, fAngleZ)
    PrepCorner(6, fCorner7X, fCorner7Y, fAngleZ)
    PrepCorner(7, fCorner8X, fCorner8Y, fAngleZ)

    ; Find the min/max X and Y coordinates for the corners
    ; actually used by the volume, starting from the center
    ; of the volume.

    fVolMaxX = Self.X
    fVolMinX = Self.X

    fVolMaxY = Self.Y
    fVolMinY = Self.Y

    Int i = 0

    while (i < iCornerCount)
        fVolMaxX = MaxOf(fVolMaxX,fVolCornerX[i])
        fVolMinX = MinOf(fVolMinX,fVolCornerX[i])

        fVolMaxY = MaxOf(fVolMaxY,fVolCornerY[i])
        fVolMinY = MinOf(fVolMinY,fVolCornerY[i])

        i += 1
    endwhile

;   Debug.Trace(Self + " is at X=" + Self.X + ", Y=" + Self.Y + ", Z=" + Self.Z + ", Angle=" + fAngleZ)
;   Debug.Trace(Self + " fVolumeScale is " + fVolumeScale)
;   Debug.Trace(Self + " Z range is " + fVolMinZ + " to " + fVolMaxZ)
;   Debug.Trace(Self + " X range is " + fVolMinX + " to " + fVolMaxX)
;   Debug.Trace(Self + " Y range is " + fVolMinY + " to " + fVolMaxY)

;   Debug.Trace(Self + " Corner1 is at " + fVolCornerX[0] + "," + fVolCornerY[0])
;   Debug.Trace(Self + " Corner2 is at " + fVolCornerX[1] + "," + fVolCornerY[1])
;   Debug.Trace(Self + " Corner3 is at " + fVolCornerX[2] + "," + fVolCornerY[2])
;   Debug.Trace(Self + " Corner4 is at " + fVolCornerX[3] + "," + fVolCornerY[3])
;   Debug.Trace(Self + " Corner5 is at " + fVolCornerX[4] + "," + fVolCornerY[4])
;   Debug.Trace(Self + " Corner6 is at " + fVolCornerX[5] + "," + fVolCornerY[5])
;   Debug.Trace(Self + " Corner7 is at " + fVolCornerX[6] + "," + fVolCornerY[6])
;   Debug.Trace(Self + " Corner8 is at " + fVolCornerX[7] + "," + fVolCornerY[7])

EndFunction

; ---------------------------------------------------------------

Function PrepCorner(Int aiIndex, Float afX, Float afY, Float afAngle)

    ; Scale the corner coordinates passed to us, adjust their
    ; position for the angle of the spawner, and save in the
    ; coordinate arrays at iIndex.

    Float fScaledX = (afX * fVolumeScale)
    Float fScaledY = (afY * fVolumeScale)

    Float fCos = Math.cos(afAngle)
    Float fSin = Math.sin(afAngle)

    Float fAdjustedX = (fCos * fScaledX) + (fSin * fScaledY)
    Float fAdjustedY = (fCos * fScaledY) - (fSin * fScaledX)

    fVolCornerX[aiIndex] = fAdjustedX + Self.X
    fVolCornerY[aiIndex] = fAdjustedY + Self.Y

EndFunction

; ---------------------------------------------------------------

Function SpawnInitialFishBatch()

    ; How many do we need to spawn?

    Int iFishToSpawn = iMaxFishCount - iCurrentFishCount

;   Debug.Trace(Self + " is about to spawn " + iFishToSpawn + " fish")

    ; Create that many fish.

    Int i = 0

    while (i < iFishToSpawn)
        SpawnFish()
        i += 1
    endwhile

;   Debug.Trace(Self + " has finished spawning " + iCurrentFishCount + " fish")

EndFunction

; ---------------------------------------------------------------

bool Function SpawnFish()

    ; Ensure that all counts are what they should be, and spawn
    ; one fish if so. If not, stop and turn off the loop.

    if (iCurrentFishCount < iMaxFishCount) && (iCurrentFishCount >= 0)
        SpawnFishAtRef(Self)
        iCurrentFishCount += 1
;       Debug.Trace("Spawner " + Self + " spawned fish # " + iCurrentFishCount)
        return true
    elseif iCurrentFishCount < 0 ; Current count is negative
;       Debug.Trace("(" + Self +") has negative iCurrentFishCount of " + iCurrentFishCount + ", abort!")
        bLooping = FALSE
    else ; Current count is max count or over
;       Debug.Trace("(" + Self + ") SpawnFish() failed because iCurrentFishCount is " + iCurrentFishCount)
;       Debug.Trace("(" + Self + ") iMaxFishCount: " + iMaxFishCount)
        bLooping = FALSE
    endif

EndFunction

; ---------------------------------------------------------------

Function SpawnFishAtRef(ObjectReference arSpawnRef)

    ; Spawns one fish inside the spawner ref. The type of fish
    ; is chosen at random from the FishTypes list. The reference
    ; is cast to the fpiAquariumFish base class.

    Activator FishType = FishTypes.GetAt(RandomInt(0, FishTypes.GetSize() - 1)) as Activator

    ObjectReference FishRef = arSpawnRef.PlaceAtMe(FishType, 1, false, true)
    fpiAquariumFish NewFish = FishRef as fpiAquariumFish

;   Debug.TraceConditional("Spawner " + Self + " is creating fpiAquariumFish " + NewFish, bPrintDebug)

    ; Now we pass information about our properties to the new fish, so
    ; he knows who owns him and where he can go within our volume.

    NewFish.SetInitialSpawnerProperties(Self, fVolMaxZ, fVolMinZ, fVolMaxX, fVolMinX, fVolMaxY, fVolMinY, iCornerCount, fVolCornerX, fVolCornerY)

EndFunction

; ---------------------------------------------------------------

float Function MaxOf(Float afValue1, Float afValue2)

    ; Return the greater of the two arguments. Arbitrarily
    ; return the second value if they are equal.

    Float fMaxVal

    if afValue1 > afValue2
        fMaxVal = afValue1
    else
        fMaxVal = afValue2
    endif

    return fMaxVal

EndFunction

; ---------------------------------------------------------------

float Function MinOf(Float afValue1, Float afValue2)

    ; Return the lesser of the two arguments. Arbitrarily
    ; return the second value if they are equal.

    Float fMinVal

    if afValue1 < afValue2
        fMinVal = afValue1
    else
        fMinVal = afValue2
    endif

    return fMinVal

EndFunction

 

Edit: I tried a version of the script without the schooling code. The motion is better, but it still stack dumps. I noticed the dumps are happening exactly once every 60 seconds, which seems a little odd.

 

Edit2: It appears this may be something specific to the particular character I was using to test. I tried several other characters, and none of them had any stack dumps. Strange.

Edited by BrettM
Link to comment
Share on other sites

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.
Note: Your post will require moderator approval before it will be visible.

Guest
Reply to this topic...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

 Share

×
×
  • Create New...