scriptName CritterSpawn extends ObjectReference 

import Critter
import Utility

;----------------------------------------------
; Properties to be set for this Critter spawn
;----------------------------------------------

; The type of critter (base object) to create
FormList property CritterTypes auto
{ The base object to create references of to spawn critters}

; The distance from this spawner that Moths are allowed to be
float property fLeashLength = 500.0 auto
{ The distance that moths are allowed to be from this spawner}
float property fLeashHeight = 50.0 auto
{ The distance that dragonflies are allowed to be from above spawner}
float property fLeashDepth = 50.0 auto
{ The distance that fish are allowed to be from below spawner}
float property fMaxPlayerDistance = 2000.0 auto
{ The distance from the player before the Spawner stops spawning critters}

int property iMaxCritterCount = 10 auto
{ The maximum number of critters this spawner can generate}
float property fFastSpawnInterval = 0.1 auto
{ When spawning critters, the interval between spawns}
float property fSlowSpawnInterval = 5.0 auto
{ When spawning critters, the interval between spawns}

GlobalVariable property GameHour auto
{ Make this point to the GameHour global }
float property fStartSpawnTime = 6.0 auto
{ The Time after which this spawner can be active}
float property fEndSpawnTime = 11.0 auto
{ The Time before which this spawner can be active}

float property fLeashOverride auto
{Optional: Manually set roaming radius for critters spawned}

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)
;----------------------------------------------

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



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
		
		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 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()
	shouldTryAgain = false
	UnregisterForUpdate()
endEVENT

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
	
	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
		
		;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

 
function QueueAdditionalSpawns() 
 	VanillaLoopBreak()
	if (!isSpawning && iSpawnedCritterCount < iMaxCritterCount*10)
		UnregisterForUpdate()
		RegisterForSingleUpdate(1+ (1+iSpawnedCritterCount) * iRespawnDelay  ) 
	endif
endFunction

; Called by critters when they die
Event OnCritterDied() 
 	VanillaLoopBreak()  
	if iDeadCritterCount < iSpawnedCritterCount
		iDeadCritterCount += 1   	
	else
		;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
	; a critter died, if we're not currently spawning, try to refresh the queue or push back the delay 
	QueueAdditionalSpawns() 
endEvent

bool Function SpawnCritterAtRef(ObjectReference arSpawnRef)
	
 	if VanillaLoopBreak()  
		return false
	endif
	; Pick a random critter type
	Activator critterType = CritterTypes.GetAt(RandomInt(0, CritterTypes.GetSize() - 1)) as Activator
	
	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


float Function GetPlayerDistance() ; Caches player reference too

 	if VanillaLoopBreak()    
		return fMaxPlayerDistance + 1
	endif
	if !PlayerRef 
		PlayerRef = Game.GetPlayer()
	endif 
	return PlayerRef.GetDistance(self)
endFunction

bool Function ShouldSpawn()
 	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
		return IsActiveTime() && CustomCheck()
	else		
		shouldTryAgain = false
		return false
	endif
endFunction

function SetExtraWaitTime(float t)
	extraWaitTime = t
endFunction
function ClearExtraWaitTime()
	extraWaitTime = 0
endFunction


;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