Jump to content

Hound/worm waves spawns can duplicate for players that leave during the warning phase and return before it's over


hoxi
  • Fixed

This is due for years now, though I wish I didn't forget to look into it so much..

The main issue lies here:

local function LoadSaveDataFromMissingSpawnInfo(player, missingspawninfo)
	-- add this
	-- can also check for _timetoattack < 5 or so, and extend the saved _timetoattack
	-- if you don't want the player to get attacked while loading or very very shortly after
	if _warning and not missingspawninfo._spawninfo then
		--print("[hounded][LoadSaveDataFromMissingSpawnInfo] Currently in warning phase and player record had no spawns set, not setting delayed spawn info.", player.name, player.userid)

		return
	end

	_delayedplayerspawninfo[player] =
	{
		_warning = missingspawninfo._warning,
		_timetoattack = missingspawninfo._timetoattack,
		_warnduration = missingspawninfo._warnduration,
		_timetonextwarningsound = missingspawninfo._timetonextwarningsound,
		_announcewarningsoundinterval = missingspawninfo._announcewarningsoundinterval,
	}
	if missingspawninfo._targetstatus then
		_targetableplayers[player.GUID] = missingspawninfo._targetstatus
	end
	if missingspawninfo._spawninfo then
		local spawninforec = missingspawninfo._spawninfo
		_delayedplayerspawninfo[player]._spawninfo =
		{
			players = {[player] = spawninforec.count},
			timetonext = spawninforec.timetonext,
			averageplayerage = spawninforec.averageplayerage,
		}
	end
end

Basically, this is what happens:

  • A player leaves during the warning phase, the info about the scheduled attack is saved for them.
  • That same player returns during the warning phase (unavoidable if there's no other players in the shard to keep the main timer going).
  • The saved info gets set to be handled on its own, but.. the player is also still being handled by the main attack.
  • This results in both the main attack and the delayed info going through, essentially running two sets of spawns for this player.
  • Note: the issue doesn't happen if the warning phase is over and spawns are determined before the player comes back.

The line I added basically just prevents the saved scheduled attack from being set if the current attack is in the warning phase, since the player can just become a part of it again at that point, unless the player left after the warning phase was over and this is the warning of another attack, in which case ignore them for the main attack and let the delayed one take over (there's a few other issues that need to be addressed to account for this, shown further below).

As noted, a check for the time to attack being too short can also be added, to in which case, let the player get handled individually outside of the main attack, and ensuring the saved time to attack is also a certain minimum, to avoid targeting the player while loading in, or very shortly after.

Lastly, it would be possible to track which attack is which with a counter that gets saved by the game, and is stored for players that leave, if you want to really really confirm when they come back that the attack is indeed the same one.

 

Anyhow, fixing the issue alone won't do, as there's other issues that need to be addressed.

 

The "water immunity" checks:

-- vanilla
local function ClearWaterImunity()
	for GUID,data in pairs(_targetableplayers) do
		_targetableplayers[GUID] = nil
	end
end

-- tweaked
local function ClearWaterImunity()
	for GUID,data in pairs(_targetableplayers) do
		local player = Ents[GUID]

		-- ensure water immunity isn't cleared if a delayed spawn is being handled for this player, let that handle it instead
		if not player or not _delayedplayerspawninfo[player] then
			_targetableplayers[GUID] = nil
		end
	end
end


-- vanilla
local function CheckForWaterImunityAllPlayers()
	for i, v in ipairs(_activeplayers) do
		CheckForWaterImunity(v)
	end
end

-- tweaked
local function CheckForWaterImunityAllPlayers()
	for i, v in ipairs(_activeplayers) do
		-- ensure water immunity isn't checked if a delayed spawn is being handled for this player, let that handle it instead
		if not _delayedplayerspawninfo[v] then
			CheckForWaterImunity(v)
		end
	end
end

(also please take a look at this report, this doesn't seem intended given that ice tiles are temporary tiles in the middle of the ocean)

 

The warning speeches:

-- vanilla
function self:DoWarningSpeech()
    for GUID,data in pairs(_targetableplayers) do
    	if data == "land" then
    		local player = Ents[GUID]
        	player:DoTaskInTime(math.random() * 2, _DoWarningSpeech)
    	end
    end
end

-- tweaked
function self:DoWarningSpeech()
	for GUID,data in pairs(_targetableplayers) do
		if data == "land" then
			local player = Ents[GUID]

			-- ignore player if they have a delayed spawn, let that handle it
			if player and not _delayedplayerspawninfo[player] then
				player:DoTaskInTime(math.random() * 2, _DoWarningSpeech)
			end
		end
	end
end

 

The warning sounds:

-- vanilla
function self:DoWarningSound()
    for k,v in pairs(self:GetWarningSoundList()) do
    	if _timetoattack <= v.time or _timetoattack == nil then
    		for GUID,data in pairs(_targetableplayers)do
    			local player = Ents[GUID]
    			if player and data == "land" then
    				player:PushEvent("houndwarning",HOUNDWARNINGTYPE[v.sound])
    			end
    		end
    		break
    	end
    end
end

-- tweaked
function self:DoWarningSound()
	for k,v in pairs(self:GetWarningSoundList()) do
		if _timetoattack <= v.time or _timetoattack == nil then
			for GUID,data in pairs(_targetableplayers)do
				if data == "land" then
					local player = Ents[GUID]

					-- ignore player if they have a delayed spawn, let that handle it
					if player and not _delayedplayerspawninfo[player] then
						player:PushEvent("houndwarning",HOUNDWARNINGTYPE[v.sound])
					end
				end
			end
			break
		end
	end
end

 

One oversight in the OnUpdate function:

-- vanilla
function self:OnUpdate(dt)
	if _spawnmode == "never" then
		return
	end

	for player, data in pairs (_delayedplayerspawninfo) do
		data._timetoattack = data._timetoattack - dt
		if data._timetoattack < 0 then
			_warning = false

-- tweaked
function self:OnUpdate(dt)
	if _spawnmode == "never" then
		return
	end

	for player, data in pairs (_delayedplayerspawninfo) do
		data._timetoattack = data._timetoattack - dt
		if data._timetoattack < 0 then
			-- correctly disable the warning in the scheduled spawn, instead of touching _warning
			data._warning = false

 

And lastly, GetWaveAmounts (very important):

-- vanilla
local function GetWaveAmounts()

	-- first bundle up the players into groups based on proximity
	-- we want to send slightly reduced hound waves when players are clumped so that
	-- the numbers aren't overwhelming
	local groupindex = {}
	local nextgroup = 1
	for i, playerA in ipairs(_activeplayers) do
		for j, playerB in ipairs(_activeplayers) do

-- tweaked
local function GetWaveAmounts()

	-- first bundle up the players into groups based on proximity
	-- we want to send slightly reduced hound waves when players are clumped so that
	-- the numbers aren't overwhelming
	local groupindex = {}
	local nextgroup = 1
	local valid_players = shallowcopy(_activeplayers)

	-- ignore players being handled by a delayed spawn
	for i, player in ipairs(valid_players) do
		if _delayedplayerspawninfo[player] then
			table.remove(valid_players, i)
		end
	end

	for i, playerA in ipairs(valid_players) do
		for j, playerB in ipairs(valid_players) do

If a player has a delayed spawn being handled (like from a previous attack if they left a while ago and came back during another one), ignore them when determining spawns. More of an edge case thing, but still worth accounting for it. This is also the second bit of why the duplicated spawns happen, there's no accounting for players with delayed spawns being handled.

 

And uh, that's about it, I apologize that this is a bit big. I attached the file override I used to test all this, if that'd make more digestible.

 

One last thing? Please look into this report comment regarding the Greater Depth Worm scheduled spawn not saving? It's been there for a good while now, it wouldn't take much to fix it.

 

Edit: yeah I figured there'd be some specifics with _wave_pre_upgraded that need to be covered with delayed spawns, so here's what I found.

 

Normally, a main attack will use the worm boss upgrade if available, set it as "used", and then on planning the next attack, it will get set to nil and the process will restart basically.

There's two issues with that. One is that if a worm boss wave happens but no spawns happen (possible due to spawnsToRelease being 0, for players too new to the server, due to another reported issue, but could also happen normally if a newer player stays and older players leave), the boss won't spawn, _wave_pre_upgraded will be set to nil, and the wave upgrade chance will still reset.

In caves.lua:

    specialupgradecheck = function(wave_pre_upgraded, wave_override_chance, _wave_override_settings)
        wave_pre_upgraded = nil -- HERE, don't set to nil, preserve if it's "available" and skip the chance steps below

        local chance = wave_override_chance * (_wave_override_settings["worm_boss"] or 1)
        if _wave_override_settings["worm_boss"] ~= 0 and (math.random() < chance or _wave_override_settings["worm_boss"] == 9999) then
            wave_pre_upgraded = "available"
        end

        if wave_pre_upgraded == "available" then
            wave_override_chance = 0
        elseif TheWorld.state.cycles > TUNING.WORM_BOSS_DAYS then
            wave_override_chance = math.min(0.5, wave_override_chance + 0.05)
        end

        return wave_pre_upgraded, wave_override_chance
    end,

 

The other issue is that a delayed player attack might spawn a worm boss, but won't set _wave_pre_upgraded to nil afterwards, meaning it'll stay as "used", which will still cause worm boss warnings and player speech to play (but no worm boss will spawn), for both main attacks, or other players with delayed worm attacks.

 

You could do something like this, or handle it through HandleSpawnInfoRec itself:

function self:OnUpdate(dt)
	if _spawnmode == "never" then
		return
	end

	for player, data in pairs (_delayedplayerspawninfo) do
		data._timetoattack = data._timetoattack - dt
		if data._timetoattack < 0 then
			data._warning = false

			-- Okay, it's hound-day, get number of dogs for each player
			if data._spawninfo == nil then
				GetDelayedPlayerWaveAmounts(player, data)
			end

			local groupsdone = {}
			CheckForWaterImunity(player)
			for i, spawninforec in ipairs(data._spawninfo) do
				local has_pre_upgrade = _wave_pre_upgraded == "available"
				HandleSpawnInfoRec(dt, i, spawninforec, groupsdone)

				-- remove upgrade if used, in case there's more than one delayed player attack
				-- so the others get the proper warnings, and to make sure the next main one doesn't incorrectly start with the wrong warning sounds
				if has_pre_upgrade and _wave_pre_upgraded == "used" then
					_wave_pre_upgraded = nil
				end
			end

 

 

hounded.lua


Steps to Reproduce

On your own, with no other players:

  • Start a server with caves.
  • Wait until you can hear the warning from a hound attack.
  • Disconnect or enter the caves.
  • Reconnect or exit the caves.
  • Notice how warning sounds and warning speeches are duplicated, and the amount of hounds that'll spawn will be doubled or so. The same thing will apply to the worms in the caves if exiting the caves or disconnecting during the warning and reentering/reconnecting.

With more players, or by spawning fake players:

  • Start a server with caves.
  • Wait until you can hear the warning from a hound attack.
  • Enter the caves.
  • Return to the surface after the warnings have stopped up there.
  • Notice how things work fine.
  • Repeat the steps but instead return to the surface while the warning is still going up there.
  • Notice the duplicated warning sounds, warning speeches, and amount of hounds.
  • Like 2



User Feedback


A developer has marked this issue as fixed. This means that the issue has been addressed in the current development build and will likely be in the next update.

this is a huge issue for console players as well and will also happen when rolling back to when a hound wave has already started

Share this comment


Link to comment
Share on other sites

Yes, rollback saves should get affected the same as with some of the other cases, it's very similar to standard saving and loading, and in this case, goes through the same process that results in the bug.

Edited by hoxi

Share this comment


Link to comment
Share on other sites

The duplicated events for saved players has been fixed. We're still investigating the Great Depths Worm issue.

Investigating, checking and testing these take a long time and sometimes we don't have that availability.

Thank you for the detailed report.

Changed Status to Fixed

  • Like 1
  • Thanks 1

Share this comment


Link to comment
Share on other sites

Oh I'm aware about not always having the time, I understand, and I've been there. It's why I try politely remind from time to time (at least mainly with bigger issues), especially if at the time there was something else big going on, like a beta or a big update/event. Thank you for looking into all this!

 

Regarding the Depth Worms issue, I believe the issue was simply caused due to this bit here that was mentioned in the report:

-- vanilla
function self:OnUpdate(dt)
	if _spawnmode == "never" then
		return
	end

	for player, data in pairs (_delayedplayerspawninfo) do
		data._timetoattack = data._timetoattack - dt
		if data._timetoattack < 0 then
			_warning = false

-- tweaked
function self:OnUpdate(dt)
	if _spawnmode == "never" then
		return
	end

	for player, data in pairs (_delayedplayerspawninfo) do
		data._timetoattack = data._timetoattack - dt
		if data._timetoattack < 0 then
			-- correctly disable the warning in the scheduled spawn, instead of touching _warning
			data._warning = false

Due to the saved record issue, it could be possible for the timer saved record timer to reach 0 and (due to the oversight) disable the main attack warning. Then, in the same update (further below in OnUpdate function), if there is supposed to be a warning for the main attack, it will set the interval to 0 and play it, which in the case of the Greater Depth Worm results in a mini quake.

This causes mini earth quakes to be queued per frame/update until either the attack fully ends for the saved record, or after the main attack timer reaches 0 and stops trying to do warnings.

Edited by hoxi
  • Like 1

Share this comment


Link to comment
Share on other sites

Thank you again for addressing all these issues.

After some testing and messing around with all these fixes in, I noticed a few more things that could be looked at, one being an oversight on my part which I'll go over near the end.

 

I noticed that with the new changes, delayed spawn info doesn't cause mini earthquakes for Greater Worm Warnings:

function self:DoDelayedWarningSound(player, data)
	for k,v in pairs(self:GetWarningSoundList(player)) do
		if data._timetoattack <= v.time or data._timetoattack == nil then
			if _targetableplayers[player.GUID] == "land" then
				player:PushEvent("houndwarning", HOUNDWARNINGTYPE[v.sound])
			end
			break
		end
	end
end

You can simply add it here like with DoWarningSound. It'd be possible to have a very new player in the caves along with someone who's been in the server for a while.

If the older player leaves the caves or the server, but the new player stays, said new player might get warnings but no spawns (which can be expected). But then if the older player rejoins, they'll get Greater Depth Worm warnings with no mini earthquakes, which is a weird inconsistency (I guess it could also look weird from the perspective of other players with mini earth quakes and no other sounds, so I'm more just pointing it out just in case it was missed and wasn't intentional).

 

 

 

Something else I only noticed now is this bit here:

	for player, data in pairs (_delayedplayerspawninfo) do
		data._timetoattack = data._timetoattack - dt
		if data._timetoattack < 0 then
			data._warning = false

			-- Okay, it's hound-day, get number of dogs for each player
			if data._spawninfo == nil then
				GetDelayedPlayerWaveAmounts(player, data)
			end

			local groupsdone = {}
			CheckForWaterImunity(player)
			for i, spawninforec in ipairs(data._spawninfo) do
				HandleSpawnInfoRec(dt, i, spawninforec, groupsdone)
			end

			for i, v in ipairs(groupsdone) do
				table.remove(data._spawninfo, v)
			end

			if #data._spawninfo <= 0 then
				_delayedplayerspawninfo[player] = nil
				_targetableplayers[player] = nil -- HERE, this should be  _targetableplayers[player.GUID] = nil
			end

 

 

 

Another issue that still remains because I forgot to link it or mention it is that, the first time you hear a hound/worm warning, sometimes nothing spawns. There was a report about this and I made a comment on it with a simple solution to it.

 

 

 

Lastly, the oversight on my part I mentioned earlier this, in the GetWaveAmounts function:

		local attackdelaybase = _attackdelayfn()
		local playerAge = player.components.age:GetAge()

		-- amount of hounds relative to our age
		-- if we never saw a warning or have lived shorter than the minimum wave delay then don't spawn hounds to us
		local playerInGame = GetTime() - player.components.age.spawntime
		local spawnsToRelease = (playerInGame > _warnduration and playerAge >= attackdelaybase) and CalcPlayerAttackSize(player) or 0

		if spawnsToRelease > 0 then

Due to saved record info being skipped during warning phases to avoid duplication, this can now result in no spawns due to the spawntime check. I feel like it'd be more proper to instead check something like this?

		local attackdelaybase = _attackdelayfn()
		local playerAge = player.components.age:GetAge()

		-- amount of hounds relative to our age
		-- if we never saw a warning or have lived shorter than the minimum wave delay then don't spawn hounds to us
		local spawnsToRelease = player.loadingprotection == nil and (playerAge >= attackdelaybase) and CalcPlayerAttackSize(player) or 0

		if spawnsToRelease > 0 then

The idea here being that players loading in won't have spawns scheduled for them. This loading protection check could also be added when iterating through _delayedplayerspawninfo in OnUpdate, to not process any info for players who are still loading in.

 

And along with that, here are other potential tweaks that could be made.

local function LoadSaveDataFromMissingSpawnInfo(player, missingspawninfo)

	if _warning and missingspawninfo._spawninfo == nil then
		return -- Currently in warning phase and player record had no spawns set, let this player be included in the current have by CheckForWaterImunityAllPlayers.
	end

	_delayedplayerspawninfo[player] =
	{
		_warning = missingspawninfo._warning,
		_timetoattack = math.max(missingspawninfo._timetoattack, 6.5), -- HERE
		_warnduration = missingspawninfo._warnduration,
		_timetonextwarningsound = missingspawninfo._timetonextwarningsound,
		_announcewarningsoundinterval = missingspawninfo._announcewarningsoundinterval,
		_wave_pre_upgraded = missingspawninfo._wave_pre_upgraded,
	}
	if missingspawninfo._targetstatus then
		_targetableplayers[player.GUID] = missingspawninfo._targetstatus
	end
	if missingspawninfo._spawninfo then
		local spawninforec = missingspawninfo._spawninfo
		_delayedplayerspawninfo[player]._spawninfo =
		{
			players = {[player] = spawninforec.count},
			timetonext = 0, -- HERE
			averageplayerage = spawninforec.averageplayerage,
		}
	end
end

Ensuring time to attack is always some value above ensure that players will get a short warning (after they load in, if the loading protection check is added when going through _delayedplayerspawninfo) even if the warning phase ended. Due to this reenabling a short warning phase, you'd want to set the time for the next spawn to 0 like how this normally works for the first spawn. Alternatively, you can make it so that if the warning phase ended for this record, just delay the next spawn a bit in the same way. Keep in mind that due to loading spawn protection, releasing spawns can result in them instantly deaggroing.

I used 6.5 as there's usually 5 second grace periods, plus 1.5 more from loading protection fadeout, but this value is up to you if you decide to add it (keep in mind that OnLoad, will set the main time to attack to the warning phase time + 5 seconds, if already in the warning phase, which is pretty generous).

A similar value could also be set for the main time to attack, in the OnPlayerJoined function, 

local function OnPlayerJoined(src, player)
	for i, v in ipairs(_activeplayers) do
		if v == player then
			return
		end
	end
	table.insert(_activeplayers, player)

	LoadPlayerSpawnInfo(player)

	-- if this is the first player to join and there's an ongoing attack warning, give them a little bit of time
	if #_activeplayers == 1 and _warning and _timetoattack < 15 then
		_timetoattack = 15
	end
end

This could even imitate what OnLoad does if you want that sort of consistency, but it's up to you.

Edited by hoxi

Share this comment


Link to comment
Share on other sites



Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now

×
  • Create New...