|
|
|
@ -1,5 +1,4 @@ |
|
|
|
|
scriptName CritterSpawn extends ObjectReference |
|
|
|
|
{MODIFIED BY STEVE40 and USKP} |
|
|
|
|
scriptName CritterSpawn extends ObjectReference |
|
|
|
|
|
|
|
|
|
import Critter |
|
|
|
|
import Utility |
|
|
|
@ -42,189 +41,292 @@ float property fLeashOverride auto |
|
|
|
|
bool property bSpawnInPrecipitation auto |
|
|
|
|
{Should this critter spawn in rain/snow? DEFAULT: FALSE} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Bool property bAllowRespawn = true auto |
|
|
|
|
Bool property bReducedRespawn = true auto |
|
|
|
|
;---------------------------------------------- |
|
|
|
|
; Constants (shouldn't need to modify these) |
|
|
|
|
;---------------------------------------------- |
|
|
|
|
float fCheckPlayerDistanceTime = 2.0 |
|
|
|
|
|
|
|
|
|
;---------------------------------------------- |
|
|
|
|
; Variables to keep track of spawned critters |
|
|
|
|
;---------------------------------------------- |
|
|
|
|
int property iCurrentCritterCount = 0 auto hidden |
|
|
|
|
bool bLooping |
|
|
|
|
bool bPrintDebug = FALSE ; should usually be set to false. |
|
|
|
|
int recursions |
|
|
|
|
|
|
|
|
|
; Do initial stuff when my 3D has loaded up. |
|
|
|
|
EVENT OnCellAttach() ; USKP 1.3.3 - Changed from OnLoad() to be more reliable, especially in interior cells. |
|
|
|
|
; The Spawner will register for update and periodically check whether the player is close or not |
|
|
|
|
; - JOEL REFACTOR - Going to use onLoad() & onUnload() instead of states? |
|
|
|
|
; GotoState("WaitingForPlayer") |
|
|
|
|
; - JOEL REFACTOR - also no longer need to update |
|
|
|
|
; RegisterForSingleUpdate(fCheckPlayerDistanceTime) |
|
|
|
|
|
|
|
|
|
if bPrintDebug == TRUE |
|
|
|
|
; debug.trace("spawner " + self + " loaded.") |
|
|
|
|
recursions = 0 |
|
|
|
|
endif |
|
|
|
|
|
|
|
|
|
; set our control bool to start the loop |
|
|
|
|
bLooping = TRUE |
|
|
|
|
|
|
|
|
|
while bLooping |
|
|
|
|
if bPrintDebug |
|
|
|
|
recursions += 1 |
|
|
|
|
; debug.trace("spawner " + self + " while loop #" + recursions) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
int property iCurrentCritterCount = 0 auto hidden ; not used, set to -1 to break vanilla spawner while loops |
|
|
|
|
bool bLooping = false ; reintroduced to catch baked runaway loops |
|
|
|
|
float extraWaitTime = 0.0 ; extra time in seconds before trying again |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
float property fCheckPlayerDistanceTime = 2.0 auto hidden |
|
|
|
|
int property iSpawnedCritterCount = 0 auto hidden |
|
|
|
|
int property iDeadCritterCount = 0 auto hidden |
|
|
|
|
int property iRespawnDelay = 6 auto hidden |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
bool property isSpawning = false auto hidden |
|
|
|
|
bool property shouldTryAgain = false auto hidden |
|
|
|
|
actor property PlayerRef auto |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
bool bPrintDebug = FALSE |
|
|
|
|
Cell _ParentCell |
|
|
|
|
|
|
|
|
|
Cell property ParentCell |
|
|
|
|
Cell function get() |
|
|
|
|
if !_ParentCell |
|
|
|
|
_ParentCell = self.GetParentCell() |
|
|
|
|
endif |
|
|
|
|
return _ParentCell |
|
|
|
|
endFunction |
|
|
|
|
endproperty |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
bool Function VanillaLoopBreak() |
|
|
|
|
|
|
|
|
|
if (bLooping || iCurrentCritterCount>0) |
|
|
|
|
; breaking OnCellAttach runaway loop in baked vanilla functions |
|
|
|
|
bLooping = false |
|
|
|
|
; breaking SpawnInitialCritterBatch runaway loop in baked vanilla and uskp functions |
|
|
|
|
iCurrentCritterCount = 0 |
|
|
|
|
|
|
|
|
|
if !shouldSpawn() |
|
|
|
|
; wait a bit, then see if the player is close again. |
|
|
|
|
; Removing this to eliminate some TPLOG spam |
|
|
|
|
; ; debug.TraceConditional("player not yet near spawner " + self, bPrintDebug) |
|
|
|
|
utility.wait(fCheckPlayerDistanceTime) |
|
|
|
|
else |
|
|
|
|
; ; debug.TraceConditional("spawner " + self + " ready to spawn!!!", bPrintDebug) |
|
|
|
|
; player must be nearby - spawn our initial critters |
|
|
|
|
; don't follow up as we no longer wish to re-generate new critters until the player leaves entirely |
|
|
|
|
spawnInitialCritterBatch() |
|
|
|
|
bLooping = FALSE |
|
|
|
|
endif |
|
|
|
|
endWhile |
|
|
|
|
|
|
|
|
|
Debug.Trace("CritterSpawn : Runaway Spawner warning :" + self); |
|
|
|
|
return true |
|
|
|
|
endif |
|
|
|
|
return false |
|
|
|
|
EndFunction |
|
|
|
|
Function SpawnInitialCritterBatch() |
|
|
|
|
VanillaLoopBreak() |
|
|
|
|
endFunction |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
EVENT OnCellAttach() |
|
|
|
|
VanillaLoopBreak() |
|
|
|
|
; the spawner will attach to a cell, this can be the cell we just teleported to (door) or one that loaded in the distance |
|
|
|
|
; shouldSpawn() will check if the spawner should start spawning critters, wait a bit or do nothing |
|
|
|
|
iSpawnedCritterCount = 0 |
|
|
|
|
iDeadCritterCount = 0 |
|
|
|
|
isSpawning = false |
|
|
|
|
|
|
|
|
|
shouldTryAgain = true |
|
|
|
|
|
|
|
|
|
if shouldSpawn() |
|
|
|
|
SpawnABatchOfCritters() |
|
|
|
|
elseif shouldTryAgain |
|
|
|
|
;randomize the update time so we're less likely to have synchronized spawners asking for updates |
|
|
|
|
RegisterForSingleUpdate(fCheckPlayerDistanceTime * Randomfloat(1.0,1.5)) |
|
|
|
|
|
|
|
|
|
endif |
|
|
|
|
endEVENT |
|
|
|
|
|
|
|
|
|
EVENT onUnload() |
|
|
|
|
; when our 3D unloads, stop looping until loaded again. |
|
|
|
|
bLooping = FALSE |
|
|
|
|
; ; debug.TraceConditional("spawner " + self + " unloading due to onUnload() EVENT.", bPrintDebug) |
|
|
|
|
event OnUpdate() |
|
|
|
|
|
|
|
|
|
VanillaLoopBreak() |
|
|
|
|
if (iSpawnedCritterCount < iMaxCritterCount*10) |
|
|
|
|
if shouldSpawn() |
|
|
|
|
SpawnABatchOfCritters() |
|
|
|
|
elseif shouldTryAgain |
|
|
|
|
RegisterForSingleUpdate(fCheckPlayerDistanceTime + extraWaitTime ) |
|
|
|
|
endif |
|
|
|
|
endif |
|
|
|
|
endEvent |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
EVENT onUnload() |
|
|
|
|
shouldTryAgain = false |
|
|
|
|
UnregisterForUpdate() |
|
|
|
|
endEVENT |
|
|
|
|
|
|
|
|
|
EVENT onCellDetach() |
|
|
|
|
bLooping = FALSE |
|
|
|
|
; ; debug.TraceConditional("spawner " + self + " unloading due to onCellDetach() EVENT.", bPrintDebug) |
|
|
|
|
shouldTryAgain = false |
|
|
|
|
UnregisterForUpdate() |
|
|
|
|
endEVENT |
|
|
|
|
|
|
|
|
|
Function SpawnInitialCritterBatch() |
|
|
|
|
; How many do we need to spawn? |
|
|
|
|
int icrittersToSpawn = iMaxCritterCount - iCurrentCritterCount |
|
|
|
|
Function SpawnABatchOfCritters() |
|
|
|
|
VanillaLoopBreak() |
|
|
|
|
; Important note here, even if the instance locking this thread gets dumped midway leaving the thread locked |
|
|
|
|
; this wouldn't leave threads wait()ing in the stacks, they'll just keep reregistering every iSpawnedCritterCount |
|
|
|
|
; and just won't spawn anything, the whole thing will be fixed when the spawner is unloaded and loaded again |
|
|
|
|
|
|
|
|
|
; Create that many critters |
|
|
|
|
int i = 0; |
|
|
|
|
while (i < icrittersToSpawn) |
|
|
|
|
; Create one critter at a time |
|
|
|
|
SpawnCritter() |
|
|
|
|
|
|
|
|
|
; Wait a bit before the next spawn |
|
|
|
|
;Wait(fFastSpawnInterval) |
|
|
|
|
if (! isSpawning && iSpawnedCritterCount < iMaxCritterCount*10) |
|
|
|
|
isSpawning = true |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
;if not using the Fixed critter script there's no way to prevent over-reported critter deaths, but we'll cap spawns anyway to the max value |
|
|
|
|
if (iDeadCritterCount > iSpawnedCritterCount) |
|
|
|
|
iDeadCritterCount = iSpawnedCritterCount |
|
|
|
|
endIf |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
int spawnAttempts = iMaxCritterCount - iSpawnedCritterCount + iDeadCritterCount |
|
|
|
|
|
|
|
|
|
; Next |
|
|
|
|
i = i + 1 |
|
|
|
|
endWhile |
|
|
|
|
;limiting amount of respawns by distance |
|
|
|
|
if (iDeadCritterCount>0 && bReducedRespawn) |
|
|
|
|
spawnAttempts = 1 |
|
|
|
|
endif |
|
|
|
|
|
|
|
|
|
while (spawnAttempts) |
|
|
|
|
if (SpawnCritterAtRef(self)) |
|
|
|
|
iSpawnedCritterCount += 1 |
|
|
|
|
endif |
|
|
|
|
spawnAttempts -=1 |
|
|
|
|
endWhile |
|
|
|
|
|
|
|
|
|
isSpawning = false |
|
|
|
|
if (iMaxCritterCount - iSpawnedCritterCount + iDeadCritterCount) |
|
|
|
|
;we couldn't spawn enough critters, or the player is currently killing them : try a bit later |
|
|
|
|
QueueAdditionalSpawns() |
|
|
|
|
endIf |
|
|
|
|
endIf |
|
|
|
|
endFunction |
|
|
|
|
|
|
|
|
|
; Called by critters when they die |
|
|
|
|
Event OnCritterDied() |
|
|
|
|
; Decrement current critter count, next time OnUpdate |
|
|
|
|
; gets called, we'll spawn a new one |
|
|
|
|
if iCurrentCritterCount > 0 |
|
|
|
|
iCurrentCritterCount -= 1; faster [USKP 2.0.1] |
|
|
|
|
elseif iCurrentCritterCount < 0 |
|
|
|
|
; iCurrentCritterCount must be in the negatives. Something is up, but for now increment towards zero |
|
|
|
|
iCurrentCritterCount = 0; earler saves with failed deletions [USKP 2.0.1] |
|
|
|
|
|
|
|
|
|
function QueueAdditionalSpawns() |
|
|
|
|
VanillaLoopBreak() |
|
|
|
|
if (!isSpawning && iSpawnedCritterCount < iMaxCritterCount*10) |
|
|
|
|
UnregisterForUpdate() |
|
|
|
|
RegisterForSingleUpdate(1+ (1+iSpawnedCritterCount) * iRespawnDelay ) |
|
|
|
|
endif |
|
|
|
|
endEvent |
|
|
|
|
endFunction |
|
|
|
|
|
|
|
|
|
; Spawns one Critter |
|
|
|
|
bool Function SpawnCritter() |
|
|
|
|
if (iCurrentCritterCount < iMaxCritterCount) && (iCurrentCritterCount >= 0) |
|
|
|
|
; Go ahead with the actual spawn |
|
|
|
|
return SpawnCritterAtRef(self) |
|
|
|
|
;/// [USKP 2.0.1] moved into SpawnCritterAtRef |
|
|
|
|
; Increment count |
|
|
|
|
iCurrentCritterCount = iCurrentCritterCount + 1 |
|
|
|
|
return true |
|
|
|
|
///; |
|
|
|
|
elseif iCurrentCritterCount < 0 |
|
|
|
|
; debug.trace("("+self+") has invalid iCurrentCritterCount of "+iCurrentCritterCount+", abort!") |
|
|
|
|
; turn off loop |
|
|
|
|
bLooping = FALSE |
|
|
|
|
; Called by critters when they die |
|
|
|
|
Event OnCritterDied() |
|
|
|
|
VanillaLoopBreak() |
|
|
|
|
if iDeadCritterCount < iSpawnedCritterCount |
|
|
|
|
iDeadCritterCount += 1 |
|
|
|
|
else |
|
|
|
|
; debug.trace("("+self+") SpawnCritter() failed because iCurrentCritterCount is "+iCurrentCritterCount) |
|
|
|
|
; ;debug.trace("("+self+") iMaxCritterCount: "+iMaxCritterCount) |
|
|
|
|
;iDeadCritterCount is overflowing for some reason |
|
|
|
|
;increase the spawned Critter count instead so the queue keeps getting postponed until the script shuts down |
|
|
|
|
iSpawnedCritterCount = iDeadCritterCount |
|
|
|
|
Debug.Trace("CritterSpawn : iDeadCritterCount overflowing " + iDeadCritterCount); |
|
|
|
|
|
|
|
|
|
endif |
|
|
|
|
|
|
|
|
|
return False ; [USKP 1.3.1] to help eliminate log spam and potential long term save bloating. |
|
|
|
|
endFunction |
|
|
|
|
; a critter died, if we're not currently spawning, try to refresh the queue or push back the delay |
|
|
|
|
QueueAdditionalSpawns() |
|
|
|
|
endEvent |
|
|
|
|
|
|
|
|
|
; Spawns one Critter at a specific location |
|
|
|
|
; returns true on success [USKP 2.0.1] |
|
|
|
|
; |
|
|
|
|
bool Function SpawnCritterAtRef(ObjectReference arSpawnRef) |
|
|
|
|
; Pick a random critter type |
|
|
|
|
Activator critterType = CritterTypes.GetAt(RandomInt(0, CritterTypes.GetSize() - 1)) as Activator |
|
|
|
|
|
|
|
|
|
if critterType == none |
|
|
|
|
; STEVE40+USKP extra check |
|
|
|
|
if VanillaLoopBreak() |
|
|
|
|
return false |
|
|
|
|
endif |
|
|
|
|
|
|
|
|
|
; Create the critter and cast it to the critter base class |
|
|
|
|
ObjectReference critterRef = arSpawnRef.PlaceAtMe(critterType, 1, false, true) |
|
|
|
|
Critter thecritter = critterRef as Critter |
|
|
|
|
; Pick a random critter type |
|
|
|
|
Activator critterType = CritterTypes.GetAt(RandomInt(0, CritterTypes.GetSize() - 1)) as Activator |
|
|
|
|
|
|
|
|
|
if thecritter == none |
|
|
|
|
; STEVE40+USKP extra check |
|
|
|
|
if critterType |
|
|
|
|
Critter critty = arSpawnRef.PlaceAtMe(critterType, 1, false, true) as Critter |
|
|
|
|
if critty |
|
|
|
|
critty.SetInitialSpawnerProperties(fLeashLength, fLeashHeight, fLeashDepth, fMaxPlayerDistance + fLeashLength, self) |
|
|
|
|
return true |
|
|
|
|
endif |
|
|
|
|
else |
|
|
|
|
Debug.Trace("CritterSpawn :" + arSpawnRef + " attempted to spawn a bad critter type, check the contents of " + CritterTypes ); |
|
|
|
|
return false |
|
|
|
|
endif |
|
|
|
|
|
|
|
|
|
return false |
|
|
|
|
|
|
|
|
|
endFunction |
|
|
|
|
|
|
|
|
|
; Set initial variables on the critter |
|
|
|
|
; ; Debug.TraceConditional("Spawner " + self + " is creating Critter " + thecritter, bPrintDebug); |
|
|
|
|
thecritter.SetInitialSpawnerProperties(fLeashLength, fLeashHeight, fLeashDepth, fMaxPlayerDistance + fLeashLength, self) |
|
|
|
|
|
|
|
|
|
; Increment count [USKP 2.0.1] |
|
|
|
|
iCurrentCritterCount += 1 |
|
|
|
|
return true |
|
|
|
|
endFunction |
|
|
|
|
float Function GetPlayerDistance() ; Caches player reference too |
|
|
|
|
|
|
|
|
|
; Utility method that returns the player's distance |
|
|
|
|
float Function GetPlayerDistance() |
|
|
|
|
return Game.GetPlayer().GetDistance(self) |
|
|
|
|
if VanillaLoopBreak() |
|
|
|
|
return fMaxPlayerDistance + 1 |
|
|
|
|
endif |
|
|
|
|
if !PlayerRef |
|
|
|
|
PlayerRef = Game.GetPlayer() |
|
|
|
|
endif |
|
|
|
|
return PlayerRef.GetDistance(self) |
|
|
|
|
endFunction |
|
|
|
|
|
|
|
|
|
; Utility method that tells the spawner whether it should spawn critters |
|
|
|
|
bool Function ShouldSpawn() |
|
|
|
|
;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 |
|
|
|
|
; GetParentCell check first helps avoid error log in exteriors? [USKP 2.0.1] |
|
|
|
|
if self.GetParentCell() && self.is3dLoaded() |
|
|
|
|
if !(GetPlayerDistance() <= fMaxPlayerDistance) |
|
|
|
|
if VanillaLoopBreak() |
|
|
|
|
return false |
|
|
|
|
endif |
|
|
|
|
if (iSpawnedCritterCount >= iMaxCritterCount*10) |
|
|
|
|
shouldTryAgain = false |
|
|
|
|
return false |
|
|
|
|
endif |
|
|
|
|
|
|
|
|
|
if (!bAllowRespawn && (iSpawnedCritterCount >= iMaxCritterCount)) |
|
|
|
|
shouldTryAgain = false |
|
|
|
|
return false |
|
|
|
|
endif |
|
|
|
|
|
|
|
|
|
if ParentCell != none |
|
|
|
|
float distance = GetPlayerDistance() |
|
|
|
|
if !self.is3dLoaded() || ( distance > fMaxPlayerDistance) |
|
|
|
|
extraWaitTime = 2.0 |
|
|
|
|
return false |
|
|
|
|
else |
|
|
|
|
if (bReducedRespawn && iDeadCritterCount > 0 && distance <= fMaxPlayerDistance*0.75 ) |
|
|
|
|
if (Randomfloat(1 )>0.99 && !PlayerRef.HasLos(self)) |
|
|
|
|
extraWaitTime = 0.0 |
|
|
|
|
else |
|
|
|
|
extraWaitTime = 2.0 |
|
|
|
|
return false |
|
|
|
|
endif |
|
|
|
|
else |
|
|
|
|
extraWaitTime = 0.0 |
|
|
|
|
endif |
|
|
|
|
endIf |
|
|
|
|
; Otherwise, base value on time of day (no handling of wrap around though...) |
|
|
|
|
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 IsActiveTime() && CustomCheck() |
|
|
|
|
else |
|
|
|
|
shouldTryAgain = false |
|
|
|
|
return false |
|
|
|
|
endif |
|
|
|
|
endFunction |
|
|
|
|
|
|
|
|
|
bool Function IsActiveTime() |
|
|
|
|
bool binTimeRange = false |
|
|
|
|
function SetExtraWaitTime(float t) |
|
|
|
|
extraWaitTime = t |
|
|
|
|
endFunction |
|
|
|
|
function ClearExtraWaitTime() |
|
|
|
|
extraWaitTime = 0 |
|
|
|
|
endFunction |
|
|
|
|
|
|
|
|
|
; BUGFIX BY STEVE40 - I found one case in Hearthfire where Bethesda didn't set the GameHour property on the script, causing it to spam the logs |
|
|
|
|
if GameHour == none |
|
|
|
|
bLooping = False ; kill the script |
|
|
|
|
return false |
|
|
|
|
endIf |
|
|
|
|
|
|
|
|
|
if (fEndSpawnTime >= fStartSpawnTime) |
|
|
|
|
binTimeRange = (GameHour.GetValue() >= fStartSpawnTime) && (GameHour.GetValue() < fEndSpawnTime) |
|
|
|
|
else |
|
|
|
|
binTimeRange = (GameHour.GetValue() >= fStartSpawnTime) || (GameHour.GetValue() < fEndSpawnTime) |
|
|
|
|
endIf |
|
|
|
|
return binTimeRange && ((Weather.GetCurrentWeather() == none) || \ |
|
|
|
|
(Weather.GetCurrentWeather().GetClassification() < 2) || \ |
|
|
|
|
(bSpawnInPrecipitation == TRUE)) |
|
|
|
|
;Custom method to override in custom scripts |
|
|
|
|
bool Function CustomCheck() |
|
|
|
|
return true |
|
|
|
|
endFunction |
|
|
|
|
|
|
|
|
|
bool Function IsActiveTime() |
|
|
|
|
if VanillaLoopBreak() |
|
|
|
|
return false |
|
|
|
|
endif |
|
|
|
|
bool binTimeRange = (fEndSpawnTime != fStartSpawnTime) ;dont bother reading or checking for Gamehour if not timeframe is set |
|
|
|
|
|
|
|
|
|
if (binTimeRange) |
|
|
|
|
if GameHour |
|
|
|
|
float GameHourf = GameHour.GetValue() |
|
|
|
|
if (fEndSpawnTime >= fStartSpawnTime) |
|
|
|
|
binTimeRange = (GameHourf >= fStartSpawnTime) && (GameHourf < fEndSpawnTime) |
|
|
|
|
else |
|
|
|
|
binTimeRange = (GameHourf >= fStartSpawnTime) || (GameHourf < fEndSpawnTime) |
|
|
|
|
endIf |
|
|
|
|
else |
|
|
|
|
shouldTryAgain = false ;spawner not set up properly, stop it |
|
|
|
|
Debug.Trace("CritterSpawn :" + self + " spawner not set up properly, has a timerane while missing the GameHourf property"); |
|
|
|
|
|
|
|
|
|
return false |
|
|
|
|
endif |
|
|
|
|
endif |
|
|
|
|
|
|
|
|
|
if (binTimeRange) |
|
|
|
|
if (bSpawnInPrecipitation) ;dont check for weather condition if not needed |
|
|
|
|
return true; |
|
|
|
|
else |
|
|
|
|
Weather W = Weather.GetCurrentWeather() |
|
|
|
|
return !W || (W.GetClassification() < 2) |
|
|
|
|
endif |
|
|
|
|
endif |
|
|
|
|
|
|
|
|
|
return false |
|
|
|
|
endFunction |
|
|
|
|