diff --git a/PublicationContent/description.txt b/PublicationContent/description.txt index 6cf2ebe..4d4ea8e 100644 --- a/PublicationContent/description.txt +++ b/PublicationContent/description.txt @@ -56,7 +56,6 @@ Use the [b][ZedSpawner.SpawnListBossWaves][/b] and [b][ZedSpawner.SpawnListSpeci [*][b]Probability[/b] - the chance (%) of each spawn (1-100). [*][b]SpawnCountBase[/b] - The base number of zeds to spawn, aka the number of zeds that will be spawned on the first cycle with one player. Can be adjusted by modifiers, number of players and cycle number. [*][b]SingleSpawnLimit[/b] - maximum number of zeds for one spawn. Can be adjusted by modifiers, number of players and cycle number. -[*][b]bSpawnAtPlayerStart[/b] - exactly what is written. [/list] [h1]Spawn logic[/h1] @@ -65,7 +64,5 @@ I really tried to describe in text how it works, but every time I got some kind [h1]📌[url=https://redirect.genzmey.su/kf2-zedspawner-calc]Spawn calculator[/url][/h1] [i]Just please try not to interfere with each other if you see that someone is already using a calculator.[/i] -By the way, ZedSpawner logs everything it does, so reading the logs can also help you figure out how it works. - [h1]Sources[/h1] [url=https://github.com/GenZmeY/KF2-ZedSpawner]https://github.com/GenZmeY/KF2-ZedSpawner[/url] (GNU GPLv3) diff --git a/PublicationContent/previewFull.gif b/PublicationContent/previewFull.gif new file mode 100644 index 0000000..f0affef Binary files /dev/null and b/PublicationContent/previewFull.gif differ diff --git a/ZedSpawner/Classes/SpawnAtPlayerStart.uc b/ZedSpawner/Classes/SpawnAtPlayerStart.uc new file mode 100644 index 0000000..5bff413 --- /dev/null +++ b/ZedSpawner/Classes/SpawnAtPlayerStart.uc @@ -0,0 +1,77 @@ +class SpawnAtPlayerStart extends Object + dependson(ZedSpawner) + config(ZedSpawner); + +var private config Array ZedClass; +var public config Array Map; + +public static function InitConfig(int Version, int LatestVersion) +{ + switch (Version) + { + case `NO_CONFIG: + case 2: + ApplyDefault(); + + default: break; + } + + if (LatestVersion != Version) + { + StaticSaveConfig(); + } +} + +private static function ApplyDefault() +{ + default.ZedClass.Length = 0; + default.ZedClass.AddItem("HL2Monsters.Combine_Strider"); + default.ZedClass.AddItem("HL2Monsters.Combine_Gunship"); + default.ZedClass.AddItem("HL2Monsters.Hunter_Chopper"); + default.ZedClass.AddItem("SomePackage.SomeZedClassYouWantToSpawnAtPlayerStart"); + + default.Map.Length = 0; + default.Map.AddItem("KF-SomeMapNameWhereYouWantSpawnZedsAtPlayerStart"); +} + +public static function Array > Load(E_LogLevel LogLevel) +{ + local Array > ZedList; + local class KFPMC; + local String ZedClassTmp; + local int Line, Loaded; + + Loaded = 0; + + `ZS_Info("Load zeds to spawn at player start:"); + foreach default.ZedClass(ZedClassTmp, Line) + { + KFPMC = class(DynamicLoadObject(ZedClassTmp, class'Class')); + if (KFPMC == None) + { + `ZS_Warn("[" $ Line + 1 $ "]" @ "Can't load zed class:" @ ZedClassTmp); + } + else + { + Loaded++; + ZedList.AddItem(KFPMC); + `ZS_Debug("[" $ Line + 1 $ "]" @ "Loaded successfully:" @ ZedClassTmp); + } + } + + if (Loaded == default.ZedClass.Length) + { + `ZS_Info("Spawn at player start list (Zeds) loaded successfully (" $ default.ZedClass.Length @ "entries)"); + } + else + { + `ZS_Info("Spawn at player start list (Zeds): loaded" @ Loaded @ "of" @ default.ZedClass.Length @ "entries"); + } + + return ZedList; +} + +defaultproperties +{ + +} diff --git a/ZedSpawner/Classes/SpawnListBossWaves.uc b/ZedSpawner/Classes/SpawnListBossWaves.uc index a93d939..e981544 100644 --- a/ZedSpawner/Classes/SpawnListBossWaves.uc +++ b/ZedSpawner/Classes/SpawnListBossWaves.uc @@ -10,7 +10,6 @@ struct S_SpawnEntryCfg var int Probability; var int SpawnCountBase; var int SingleSpawnLimit; - var bool bSpawnAtPlayerStart; }; var public config bool bStopRegularSpawn; @@ -47,7 +46,6 @@ private static function ApplyDefault(KFGI_Access KFGIA) SpawnEntry.SingleSpawnLimit = 1; SpawnEntry.Delay = 30; SpawnEntry.Probability = 100; - SpawnEntry.bSpawnAtPlayerStart = false; KFPM_Bosses = KFGIA.GetAIBossClassList(); foreach KFPM_Bosses(KFPMC) { @@ -65,7 +63,7 @@ public static function Array Load(E_LogLevel LogLevel) local bool Errors; local int Loaded; - `ZS_Info("Load boss waves spawn list..."); + `ZS_Info("Load boss waves spawn list:"); foreach default.Spawn(SpawnEntryCfg, Line) { Errors = false; @@ -114,8 +112,6 @@ public static function Array Load(E_LogLevel LogLevel) Errors = true; } - SpawnEntry.SpawnAtPlayerStart = SpawnEntryCfg.bSpawnAtPlayerStart; - if (!Errors) { Loaded++; @@ -126,7 +122,7 @@ public static function Array Load(E_LogLevel LogLevel) if (Loaded == default.Spawn.Length) { - `ZS_Info("Boss spawn list loaded successfully"); + `ZS_Info("Boss spawn list loaded successfully (" $ default.Spawn.Length @ "entries)"); } else { diff --git a/ZedSpawner/Classes/SpawnListRegular.uc b/ZedSpawner/Classes/SpawnListRegular.uc index e107333..dc028b8 100644 --- a/ZedSpawner/Classes/SpawnListRegular.uc +++ b/ZedSpawner/Classes/SpawnListRegular.uc @@ -11,11 +11,15 @@ struct S_SpawnEntryCfg var int Probability; var int SpawnCountBase; var int SingleSpawnLimit; - var bool bSpawnAtPlayerStart; }; var public config Array Spawn; +delegate int SpawnListSort(S_SpawnEntryCfg A, S_SpawnEntryCfg B) +{ + return A.Wave > B.Wave ? -1 : 0; +} + public static function InitConfig(int Version, int LatestVersion, KFGI_Access KFGIA) { switch (Version) @@ -46,8 +50,7 @@ private static function ApplyDefault(KFGI_Access KFGIA) SpawnEntry.RelativeStart = 25; SpawnEntry.Delay = 60; SpawnEntry.Probability = 100; - SpawnEntry.bSpawnAtPlayerStart = false; - + KFPM_Zeds = KFGIA.GetAIClassList(); foreach KFPM_Zeds(KFPMC) { @@ -120,8 +123,6 @@ public static function Array Load(E_LogLevel LogLevel) Errors = true; } - SpawnEntry.SpawnAtPlayerStart = SpawnEntryCfg.bSpawnAtPlayerStart; - if (!Errors) { Loaded++; @@ -130,9 +131,11 @@ public static function Array Load(E_LogLevel LogLevel) } } + default.Spawn.Sort(SpawnListSort); + if (Loaded == default.Spawn.Length) { - `ZS_Info("Regular spawn list loaded successfully"); + `ZS_Info("Regular spawn list loaded successfully (" $ default.Spawn.Length @ "entries)"); } else { diff --git a/ZedSpawner/Classes/SpawnListSpecialWaves.uc b/ZedSpawner/Classes/SpawnListSpecialWaves.uc index c2ebd8e..941f1c4 100644 --- a/ZedSpawner/Classes/SpawnListSpecialWaves.uc +++ b/ZedSpawner/Classes/SpawnListSpecialWaves.uc @@ -11,7 +11,6 @@ struct S_SpawnEntryCfg var int Probability; var int SpawnCountBase; var int SingleSpawnLimit; - var bool bSpawnAtPlayerStart; }; var public config bool bStopRegularSpawn; @@ -46,7 +45,6 @@ private static function ApplyDefault() SpawnEntry.RelativeStart = 0; SpawnEntry.Delay = 60; SpawnEntry.Probability = 100; - SpawnEntry.bSpawnAtPlayerStart = false; foreach class'KFGameInfo_Endless'.default.SpecialWaveTypes(AIType) { SpawnEntry.Wave = AIType; @@ -123,8 +121,6 @@ public static function Array Load(KFGameInfo_Endless KFGIE, E_LogL Errors = true; } - SpawnEntry.SpawnAtPlayerStart = SpawnEntryCfg.bSpawnAtPlayerStart; - if (!Errors) { Loaded++; @@ -135,7 +131,7 @@ public static function Array Load(KFGameInfo_Endless KFGIE, E_LogL if (Loaded == default.Spawn.Length) { - `ZS_Info("Special spawn list loaded successfully"); + `ZS_Info("Special spawn list loaded successfully (" $ default.Spawn.Length @ "entries)"); } else { diff --git a/ZedSpawner/Classes/ZedSpawner.uc b/ZedSpawner/Classes/ZedSpawner.uc index b51d05d..a283f5d 100644 --- a/ZedSpawner/Classes/ZedSpawner.uc +++ b/ZedSpawner/Classes/ZedSpawner.uc @@ -1,12 +1,13 @@ class ZedSpawner extends Info config(ZedSpawner); -const LatestVersion = 2; +const LatestVersion = 3; -const CfgSpawn = class'Spawn'; -const CfgSpawnListRW = class'SpawnListRegular'; -const CfgSpawnListBW = class'SpawnListBossWaves'; -const CfgSpawnListSW = class'SpawnListSpecialWaves'; +const CfgSpawn = class'Spawn'; +const CfgSpawnAtPlayerStart = class'SpawnAtPlayerStart'; +const CfgSpawnListRW = class'SpawnListRegular'; +const CfgSpawnListBW = class'SpawnListBossWaves'; +const CfgSpawnListSW = class'SpawnListSpecialWaves'; enum E_LogLevel { @@ -35,7 +36,6 @@ struct S_SpawnEntry var float Delay; var int PawnsLeft; var int PawnsTotal; - var bool SpawnAtPlayerStart; var bool ForceSpawn; var String ZedNameFiller; }; @@ -55,6 +55,7 @@ var private bool NoFreeSpawnSlots; var private bool UseRegularSpawnList; var private bool UseBossSpawnList; var private bool UseSpecialSpawnList; +var private bool GlobalSpawnAtPlayerStart; var private KFGameInfo_Survival KFGIS; var private KFGameInfo_Endless KFGIE; @@ -69,10 +70,18 @@ var private int WaveTotalAI; var private class CurrentBossClass; var private Array > CustomZeds; +var private Array > SpawnAtPlayerStartZeds; var private bool SpawnActive; var private String SpawnListsComment; +var private Array RepLinks; + +public simulated function bool SafeDestroy() +{ + return (bPendingDelete || bDeleteMe || Destroy()); +} + public event PreBeginPlay() { `ZS_Trace(`Location); @@ -80,7 +89,7 @@ public event PreBeginPlay() if (WorldInfo.NetMode == NM_Client) { `ZS_Fatal("NetMode == NM_Client, Destroy..."); - Destroy(); + SafeDestroy(); return; } @@ -91,7 +100,7 @@ public event PostBeginPlay() { `ZS_Trace(`Location); - if (bPendingDelete) return; + if (bPendingDelete || bDeleteMe) return; Super.PostBeginPlay(); @@ -102,12 +111,12 @@ private function InitConfig() { if (Version == `NO_CONFIG) { - Tickrate = 1.0f; LogLevel = LL_Info; SaveConfig(); } CfgSpawn.static.InitConfig(Version, LatestVersion); + CfgSpawnAtPlayerStart.static.InitConfig(Version, LatestVersion); CfgSpawnListRW.static.InitConfig(Version, LatestVersion, KFGIA); CfgSpawnListBW.static.InitConfig(Version, LatestVersion, KFGIA); CfgSpawnListSW.static.InitConfig(Version, LatestVersion); @@ -119,6 +128,8 @@ private function InitConfig() case 1: Tickrate = 1.0f; + + case 2: case MaxInt: `ZS_Info("Config updated to version"@LatestVersion); @@ -145,6 +156,7 @@ private function InitConfig() private function Init() { local S_SpawnEntry SE; + local String CurrentMap; `ZS_Trace(`Location); @@ -152,7 +164,7 @@ private function Init() if (KFGIS == None) { `ZS_Fatal("Incompatible gamemode:" @ WorldInfo.Game $ ". Destroy..."); - Destroy(); + SafeDestroy(); return; } @@ -177,7 +189,7 @@ private function Init() if (!CfgSpawn.static.Load(LogLevel) || Tickrate <= 0) { `ZS_Fatal("Wrong settings, Destroy..."); - Destroy(); + SafeDestroy(); return; } @@ -187,6 +199,11 @@ private function Init() SpawnListRW = CfgSpawnListRW.static.Load(LogLevel); SpawnListBW = CfgSpawnListBW.static.Load(LogLevel); SpawnListSW = CfgSpawnListSW.static.Load(KFGIE, LogLevel); + SpawnAtPlayerStartZeds = CfgSpawnAtPlayerStart.static.Load(LogLevel); + + CurrentMap = String(WorldInfo.GetPackageName()); + GlobalSpawnAtPlayerStart = (CfgSpawnAtPlayerStart.default.Map.Find(CurrentMap) != INDEX_NONE); + `ZS_Info("GlobalSpawnAtPlayerStart:" @ GlobalSpawnAtPlayerStart $ GlobalSpawnAtPlayerStart ? "(" $ CurrentMap $ ")" : ""); CurrentWave = INDEX_NONE; SpecialWave = INDEX_NONE; @@ -311,6 +328,7 @@ private function SetupWave() { local Array SpawnListNames; local int WaveTotalAIDef; + local byte BaseWave; local String WaveTypeInfo; local S_SpawnEntry SE; local EAIType SWType; @@ -379,9 +397,12 @@ private function SetupWave() if (UseRegularSpawnList) { SpawnListNames.AddItem("regular"); + BaseWave = KFGIS.WaveNum - CycleWaveSize * (CurrentCycle - 1); foreach SpawnListRW(SE) - if (SE.Wave == KFGIS.WaveNum - CycleWaveSize * (CurrentCycle - 1)) + if (SE.Wave == BaseWave) SpawnListCurrent.AddItem(SE); + else if (SE.Wave > BaseWave) + break; } if (UseSpecialSpawnList) @@ -426,9 +447,6 @@ private function AdjustSpawnList(out Array List) Cycle = float(CurrentCycle); Players = float(PlayerCount()); - B = float(SE.SpawnCountBase); - L = float(SE.SingleSpawnLimitDefault); - TM = CfgSpawn.default.ZedTotalMultiplier; TCM = CfgSpawn.default.SpawnTotalCycleMultiplier; TPM = CfgSpawn.default.SpawnTotalPlayerMultiplier; @@ -436,7 +454,7 @@ private function AdjustSpawnList(out Array List) LM = CfgSpawn.default.SingleSpawnLimitMultiplier; LCM = CfgSpawn.default.SingleSpawnLimitCycleMultiplier; LPM = CfgSpawn.default.SingleSpawnLimitPlayerMultiplier; - + ZedNameMaxLength = 0; foreach List(SE, Index) { @@ -455,6 +473,9 @@ private function AdjustSpawnList(out Array List) List[Index].Delay = 0.0f; } + B = float(SE.SpawnCountBase); + L = float(SE.SingleSpawnLimitDefault); + PawnTotalF = B * (TM + TCM * (Cycle - 1.0f) + TPM * (Players - 1.0f)); PawnLimitF = L * (LM + LCM * (Cycle - 1.0f) + LPM * (Players - 1.0f)); @@ -498,6 +519,7 @@ private function SpawnEntry(out Array SpawnList, int Index) local S_SpawnEntry SE; local int FreeSpawnSlots, PawnCount, Spawned; local String Action, Comment, NextSpawn; + local bool SpawnAtPlayerStart; `ZS_Trace(`Location); @@ -530,7 +552,9 @@ private function SpawnEntry(out Array SpawnList, int Index) } } - Spawned = SpawnZed(SE.ZedClass, PawnCount, SE.SpawnAtPlayerStart); + SpawnAtPlayerStart = (GlobalSpawnAtPlayerStart || (SpawnAtPlayerStartZeds.Find(SE.ZedClass) != INDEX_NONE)); + + Spawned = SpawnZed(SE.ZedClass, PawnCount, SpawnAtPlayerStart); if (Spawned == INDEX_NONE) { SpawnList[Index].Delay = 5.0f; @@ -613,9 +637,10 @@ private function Vector PlayerStartLocation() return KFGIS.FindPlayerStart(None, 0).Location; } -private function int SpawnZed(class ZedClass, int PawnCount, bool SpawnAtPlayerStart) +private function int SpawnZed(class ZedClass, int PawnCount, optional bool SpawnAtPlayerStart = false) { local Array > CustomSquad; + local ESquadType PrevDesiredSquadType; local Vector SpawnLocation, PlayerStart; local KFSpawnVolume SpawnVolume; local KFPawn_Monster KFPM; @@ -639,12 +664,18 @@ private function int SpawnZed(class ZedClass, int PawnCount, boo CustomSquad.AddItem(ZedClass); } + PrevDesiredSquadType = KFGIS.SpawnManager.DesiredSquadType; + KFGIS.SpawnManager.SetDesiredSquadTypeForZedList(CustomSquad); SpawnVolume = KFGIS.SpawnManager.GetBestSpawnVolume(CustomSquad); + KFGIS.SpawnManager.DesiredSquadType = PrevDesiredSquadType; + if (SpawnVolume == None) { return INDEX_NONE; } + SpawnVolume.VolumeChosenCount++; + SpawnLocation = SpawnVolume.Location; if (SpawnLocation == PlayerStart) { @@ -677,6 +708,11 @@ private function int SpawnZed(class ZedClass, int PawnCount, boo Spawned++; } + if (Spawned > 0) + { + KFGIS.SpawnManager.LastAISpawnVolume = SpawnVolume; + } + if (CfgSpawn.default.bShadowSpawn && !KFGIS.MyKFGRI.IsBossWave()) { KFGIS.NumAIFinishedSpawning += Spawned; @@ -690,21 +726,55 @@ private function int SpawnZed(class ZedClass, int PawnCount, boo public function NotifyLogin(Controller C) { - local ZedSpawnerRepLink RepLink; - `ZS_Trace(`Location); - - RepLink = Spawn(class'ZedSpawnerRepLink', C); - RepLink.LogLevel = LogLevel; - RepLink.CustomZeds = CustomZeds; - RepLink.ServerSync(); + + CreateRepLink(C); } public function NotifyLogout(Controller C) { `ZS_Trace(`Location); + + DestroyRepLink(C); +} + +public function CreateRepLink(Controller C) +{ + local ZedSpawnerRepLink RepLink; - return; + `ZS_Trace(`Location); + + if (C == None) return; + + RepLink = Spawn(class'ZedSpawnerRepLink', C); + RepLink.LogLevel = LogLevel; + RepLink.CustomZeds = CustomZeds; + RepLink.ZS = Self; + + RepLinks.AddItem(RepLink); + + RepLink.ServerSync(); +} + +public function bool DestroyRepLink(Controller C) +{ + local int i; + + `ZS_Trace(`Location); + + if (C == None) return false; + + for (i = RepLinks.Length - 1; i >= 0; --i) + { + if (RepLinks[i].Owner == C) + { + RepLinks[i].SafeDestroy(); + RepLinks.Remove(i, 1); + return true; + } + } + + return false; } DefaultProperties diff --git a/ZedSpawner/Classes/ZedSpawnerRepLink.uc b/ZedSpawner/Classes/ZedSpawnerRepLink.uc index 22c04b7..eab2eff 100644 --- a/ZedSpawner/Classes/ZedSpawnerRepLink.uc +++ b/ZedSpawner/Classes/ZedSpawnerRepLink.uc @@ -1,8 +1,9 @@ class ZedSpawnerRepLink extends ReplicationInfo; -var public E_LogLevel LogLevel; -var public Array > CustomZeds; -var private int Recieved; +var public ZedSpawner ZS; +var public E_LogLevel LogLevel; +var public Array > CustomZeds; +var private int Recieved; replication { @@ -10,7 +11,11 @@ replication LogLevel; } -public simulated function bool SafeDestroy() { if (!bPendingDelete) return Destroy(); else return true; } +public simulated function bool SafeDestroy() +{ + `ZS_Debug(`Location @ "bPendingDelete:" @ bPendingDelete @ "bDeleteMe" @ bDeleteMe); + return (bPendingDelete || bDeleteMe || Destroy()); +} public reliable client function ClientSync(class CustomZed) { @@ -40,11 +45,16 @@ public reliable server function ServerSync() { `ZS_Trace(`Location); + if (bPendingDelete || bDeleteMe) return; + if (CustomZeds.Length == Recieved || WorldInfo.NetMode == NM_StandAlone) { `ZS_Debug("Sync finished"); SyncFinished(); - SafeDestroy(); + if (!ZS.DestroyRepLink(Controller(Owner))) + { + SafeDestroy(); + } } else {