Redux Visibility

1
Hi all,

So I've been working on a 5065 server and I've hit a brick wall regarding the visibility of players.

I've found a bug in Redux where players will sporadically go invisible when jumping onto each others screens at specific times/angles. I've found that this bug is present on an awful lot of public sources/bases (and live servers as well), and I'm having a hard time figuring out why...

I tried following the wiki to the letter for screen and visibility, but I just haven't been able to nail down the cause or indeed, fix it. It also happens in the albetros source for example (so any sources based on these that people try and sell, have this bug).

Has anyone come across this and or can point me in the right direction of how to fix it?

Thanks I appreciate it.

Redux Visibility

4
What you need to know is:
* If the distance between you and the target is <=18 and he is not added to your screen, you spawn him with the MsgPlayer
* If the target distance is >18, you remove him from your screen but DO NOT send the MsgActon RemoveEntity packet, the client will do it alone
* You just remove the character from your client screen if he is leaving the map (teleport, disconnect etc)
* If you're Moving (MsgWalk or MsgAction:Jump) and the user is in your screen and spawned, you redirect your jump packet to your targets so they know you're moving

What the code from my repository does apart from that? Maybe you already notiiced, but when a player enter the screen it will teleport to the target location and that happens because the MsgPlayer will spawn him at the target location already, this will cause that little jump in the same coordinate. Maybe there is an easier solution, but my code calculates the line with Bresenham (Why not DDA? DDA is slower, so for that action I chose Bresenham) and creates a MsgPlayer with the first coordinate in the screen from that line, so when you send the Jump packet after that it wiill jump from the border of the screen at least.
Image

Don't PM me, I won't give Canyon 6192 complete lua or database.

Redux Visibility

5
Konichu wrote: Thu May 16, 2024 12:38 pm What you need to know is:
* If the distance between you and the target is <=18 and he is not added to your screen, you spawn him with the MsgPlayer
* If the target distance is >18, you remove him from your screen but DO NOT send the MsgActon RemoveEntity packet, the client will do it alone
* You just remove the character from your client screen if he is leaving the map (teleport, disconnect etc)
* If you're Moving (MsgWalk or MsgAction:Jump) and the user is in your screen and spawned, you redirect your jump packet to your targets so they know you're moving

What the code from my repository does apart from that? Maybe you already notiiced, but when a player enter the screen it will teleport to the target location and that happens because the MsgPlayer will spawn him at the target location already, this will cause that little jump in the same coordinate. Maybe there is an easier solution, but my code calculates the line with Bresenham (Why not DDA? DDA is slower, so for that action I chose Bresenham) and creates a MsgPlayer with the first coordinate in the screen from that line, so when you send the Jump packet after that it wiill jump from the border of the screen at least.
Thanks that's really really helpful.

My problem is (I think) - that because the client automatically despawns the entity when it leaves the screen, there's this very specific moment where if player A is chasing player B, if they jump at the same time, player A goes invisible to player B, but the visibilitydictionary is not updated because technically, player A can still see player B....

Therefore Player A is not considered a newObject, and therefore not spawned back into the screen of Player B when it arrives... Does that make sense?

I can't figure out how to reliably capture Player A and force it to be considered a newObject at that very specific moment, so that it's respawned onto the screen of Player B... :/

Redux Visibility

6
Map actions need to be synchronized. Who jumped first? Will it be removed from screen? Are you broadcasting your players movements?

If PlayerA leave PlayerB screen by jumping, you remove both from each other visibility dictionary, but you won't "despawn" them from the screen, the client will do it automatically.

If you're handling map actions asynchronously without any synchronization, then you'll face this kind of problem. First you need to handle PlayerA movement and then you handle PlayerB movement.

First update surroundings and don't broadcast the Walk or Jump packet, send them individually. Whos entering the screen needs to receive your movement packets and who's entering your screen must receive it too.

If you broadcast the packet before updating your surroundings, whos in your screen will receive it, but who's entering your screen will not. And if you broadcast them twice, you can guess what will happen.

Take a look at this:

Code: Select all

public async Task UpdateAsync(IPacket msg = null)
{
	bool isJmpMsg = false;
	ushort oldX = 0;
	ushort oldY = 0;
	var dda = new List<Point>();
	if (msg is MsgAction jump && jump.Action == ActionType.MapJump)
	{
		isJmpMsg = true;

		oldX = jump.ArgumentX;
		oldY = jump.ArgumentY;

		dda.AddRange(Bresenham.Calculate(oldX, oldY, role.X, role.Y));
	}
	else
	{
		jump = null;
	}

	List<Role> targets = role.Map.Query9BlocksByPos(role.X, role.Y);
	targets.AddRange(Roles.Values);
	foreach (Role target in targets.Select(x => x).Distinct())
	{
		bool skipMessage = false;
		if (target.Identity == role.Identity)
		{
			continue;
		}

		ushort newOldX = oldX;
		ushort newOldY = oldY;
		var isExit = false;
		var targetUser = target as Character;
		if (Calculations.GetDistance(role.X, role.Y, target.X, target.Y) <= VIEW_SIZE) // if the target is in my view range
		{
			if (Add(target)) // I try to add him to my screen, if I succeed, this means that he is a new entity in my screen, so I'll exchange spawn packets
			{
				targetUser?.Screen?.Add(role); // the target must receive me in his screen too
				if (!isJmpMsg)
				{
					skipMessage = true;
				}

				if (role is Character user)
				{
					await target.SendSpawnToAsync(user);
				}

				if (targetUser != null && isJmpMsg)
				{
					for (var i = 0; i < dda.Count; i++)
					{
						if (targetUser.GetDistance(dda[i].X, dda[i].Y) <= VIEW_SIZE)
						{
							newOldX = (ushort)dda[i].X;
							newOldY = (ushort)dda[i].Y;
							break;
						}
					}

					await role.SendSpawnToAsync(targetUser, newOldX, newOldY);
				}
				else if (targetUser != null)
				{
					await role.SendSpawnToAsync(targetUser);
				}
			}
		}
		else // if he is out of range, I will remove us from each other screen, but won't send the remove packet...
		{
			isExit = true;
			await RemoveAsync(target.Identity);
			if (targetUser?.Screen != null)
			{
				await targetUser.Screen.RemoveAsync(role.Identity);
			}
		}

		if (msg != null && targetUser != null) // in both cases, if the target is a player he must receive my movement packet
		{
			if (isJmpMsg && !isExit)
			{
				await targetUser.SendAsync(new MsgAction
				{
					Action = jump.Action,
					Argument = jump.Argument,
					X = newOldX,
					Y = newOldY,
					Command = jump.Command,
					Data = jump.Data,
					Direction = jump.Direction,
					Identity = jump.Identity,
					Map = jump.Map,
					MapColor = jump.MapColor,
					Timestamp = jump.Timestamp,
					Sprint = jump.Sprint
				});
			}
			else if (!skipMessage)
			{
				await targetUser.SendAsync(msg);
			}
		}
	}
}
Image

Don't PM me, I won't give Canyon 6192 complete lua or database.

Redux Visibility

7
Konichu wrote: Fri May 17, 2024 2:06 pm Map actions need to be synchronized. Who jumped first? Will it be removed from screen? Are you broadcasting your players movements?

If PlayerA leave PlayerB screen by jumping, you remove both from each other visibility dictionary, but you won't "despawn" them from the screen, the client will do it automatically.

If you're handling map actions asynchronously without any synchronization, then you'll face this kind of problem. First you need to handle PlayerA movement and then you handle PlayerB movement.

First update surroundings and don't broadcast the Walk or Jump packet, send them individually. Whos entering the screen needs to receive your movement packets and who's entering your screen must receive it too.

If you broadcast the packet before updating your surroundings, whos in your screen will receive it, but who's entering your screen will not. And if you broadcast them twice, you can guess what will happen.

Take a look at this:

Code: Select all

public async Task UpdateAsync(IPacket msg = null)
{
	bool isJmpMsg = false;
	ushort oldX = 0;
	ushort oldY = 0;
	var dda = new List<Point>();
	if (msg is MsgAction jump && jump.Action == ActionType.MapJump)
	{
		isJmpMsg = true;

		oldX = jump.ArgumentX;
		oldY = jump.ArgumentY;

		dda.AddRange(Bresenham.Calculate(oldX, oldY, role.X, role.Y));
	}
	else
	{
		jump = null;
	}

	List<Role> targets = role.Map.Query9BlocksByPos(role.X, role.Y);
	targets.AddRange(Roles.Values);
	foreach (Role target in targets.Select(x => x).Distinct())
	{
		bool skipMessage = false;
		if (target.Identity == role.Identity)
		{
			continue;
		}

		ushort newOldX = oldX;
		ushort newOldY = oldY;
		var isExit = false;
		var targetUser = target as Character;
		if (Calculations.GetDistance(role.X, role.Y, target.X, target.Y) <= VIEW_SIZE) // if the target is in my view range
		{
			if (Add(target)) // I try to add him to my screen, if I succeed, this means that he is a new entity in my screen, so I'll exchange spawn packets
			{
				targetUser?.Screen?.Add(role); // the target must receive me in his screen too
				if (!isJmpMsg)
				{
					skipMessage = true;
				}

				if (role is Character user)
				{
					await target.SendSpawnToAsync(user);
				}

				if (targetUser != null && isJmpMsg)
				{
					for (var i = 0; i < dda.Count; i++)
					{
						if (targetUser.GetDistance(dda[i].X, dda[i].Y) <= VIEW_SIZE)
						{
							newOldX = (ushort)dda[i].X;
							newOldY = (ushort)dda[i].Y;
							break;
						}
					}

					await role.SendSpawnToAsync(targetUser, newOldX, newOldY);
				}
				else if (targetUser != null)
				{
					await role.SendSpawnToAsync(targetUser);
				}
			}
		}
		else // if he is out of range, I will remove us from each other screen, but won't send the remove packet...
		{
			isExit = true;
			await RemoveAsync(target.Identity);
			if (targetUser?.Screen != null)
			{
				await targetUser.Screen.RemoveAsync(role.Identity);
			}
		}

		if (msg != null && targetUser != null) // in both cases, if the target is a player he must receive my movement packet
		{
			if (isJmpMsg && !isExit)
			{
				await targetUser.SendAsync(new MsgAction
				{
					Action = jump.Action,
					Argument = jump.Argument,
					X = newOldX,
					Y = newOldY,
					Command = jump.Command,
					Data = jump.Data,
					Direction = jump.Direction,
					Identity = jump.Identity,
					Map = jump.Map,
					MapColor = jump.MapColor,
					Timestamp = jump.Timestamp,
					Sprint = jump.Sprint
				});
			}
			else if (!skipMessage)
			{
				await targetUser.SendAsync(msg);
			}
		}
	}
}
Pintinho?! Haha. Thanks, again, that's very helpful :D.

I'll try some more things later and take a look at that snippet.

Redux Visibility

9
So, if we say Konichu is chasing you, then this can happen when you spawn Konichu outside of your client's screen distance with MsgPlayer. That could be a race condition where Konichu's spawn is sent to your client before it's received a response from your own jump (the client isn't updated, but the screen on the game server is). It can also happen if you're simply trying to spawn a player outside of screen distance (but I'm guessing you've triple checked those by now).

For the race condition issue, I'd recommend looking at how you're processing movement overall and how you're sending responses. Movement should generally be single-threaded per map / map bubble (most actions should be in my opinion), and the next movement shouldn't be processed until the messages for the spawns and movement response are in queue to be sent back to the client. Since messages queued and encrypted are in order, that'll ensure that the spawns and movement response messages are processed first before any follow up movements based on that server state.

Make sure you have MsgAction type 102 (or whatever QUERY_PLAYER is) implemented on your server, too. That'll at least let you heal when these occurrences happen (if you can't fix it right now). Best of luck with this.
Interested in my work?

If you wanna learn more about me and my projects: visit my portfolio website. There, you can find my free, open-source work and articles about game development. Due to contractual restrictions: I am not available for job requests or volunteer work.

About Me | GitLab Profile | Website