diff --git a/Enderal Credits.txt b/Enderal Credits.txt index 41ebb10f..dad497f6 100644 --- a/Enderal Credits.txt +++ b/Enderal Credits.txt @@ -649,6 +649,7 @@ CommonLibSSE-NG by Ryan McKenzie, powerofthree, Charmed Baryon, and others Better Dialogue Controls by ecirbaf Unofficial Enderal Port (fs.dll) by Hishutup and FelesNoctis Shorter Grass by Fhaarkas +CritterSpawn Congestion Fix by Excinerus Ghost Item Bug Fix for SkyUI by EdmanSA Drunk Sinking Head Idle Fix by KnightRangersGuild Book Covers Skyrim by DanielCoffey diff --git a/scripts/critterspawn.pex b/scripts/critterspawn.pex index 0b108151..fe876c74 100644 Binary files a/scripts/critterspawn.pex and b/scripts/critterspawn.pex differ diff --git a/source/scripts/critterspawn.psc b/source/scripts/critterspawn.psc index ae4e8595..302860e7 100644 --- a/source/scripts/critterspawn.psc +++ b/source/scripts/critterspawn.psc @@ -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