An interesting problem with a PHP-based game

PHP programming forum. Ask questions or help people concerning PHP code. Don't understand a function? Need help implementing a class? Don't understand a class? Here is where to ask. Remember to do your homework!

Moderator: General Moderators

Post Reply
User avatar
egg82
Forum Contributor
Posts: 156
Joined: Sat Oct 01, 2011 9:29 pm
Location: Colorado, USA

An interesting problem with a PHP-based game

Post by egg82 »

I am currently writing a game that uses a Flash->PHP->MySQL combo.
I'm having no issues with flash (or I wouldn't be posting on a PHP forum) but I am encountering an interesting "roadblock" with PHP.

Here's the basics of the setup:
Flash: this.ndata.load(_root.website+"/tactics/main/data.php?rand="+Math.random()*1000);

If you copy and paste the output of that line (http://gametack.org/tactics/main/data.php?rand=158.1439) into a browser's URL you will see something like this:
returned=true&unsan_array0=\m\&unsan_array1=\~\
The exact syntax is this: &variable_name=variable_contents
flash then translates this into actual variables. After that, I handle the rest with the application. Simple when the rest is done with PHP!

So here's my problem:
I obviously need to incorporate movement into my ORPG, so I think of it like this(i'll assume an "average" for non-constants):
The game runs at about 30 frames per second. I say "about" because it's not perfect and sometimes gets bogged down with information for about a quarter of a second.
Every frame your character moves anywhere from 5-7 pixels, depending on a bunch of things.
I need to make sure the players don't go over this limit (eliminating cheaters) and try not to impair a player's normal activities(and bog down the server)
So, for the raw math: 6 pixel movement*30 frames per second=180 pixels of movement per second
I ping the server with the player's new location (x, y, and z) every 250 milliseconds, or 1/4 of a second
I see it this way: character speed*FPS = player's max movement - player's new location(x+y+z)-player's old location(database's x+y+z) = player's movement
If the player's movement is greater than the player's max movement, return an error (the player's movement is multiplied by -1 if the movement is below 0)
The server will also check the time (via the date()+microtime() functions) and if the new time-the database's old time is greater than 250 milliseconds, it again returns an error
when an error is returned, the flash program quickly puts the player back into the database's x, y, and z coords so the player doesn't disconnect

This is my problem: Even at the loosest settings, I still have some issues with the server not accepting a legitimate time+coordinate input. This is a problem because attackers can easily trick the server into accepting a location far beyond the reach of any normal user.

to the raw (relevant) PHP:

ucheck.php

Code: Select all

<?php
if($good_connect == true){
	$acct_login = false;
	if((isset($_GET["user"]) and $_GET["user"] != "") and (isset($_GET["pass"]) and $_GET["pass"] != "")){
		$result = mysql_query("SELECT * FROM `accounts` WHERE `user`='".san($_GET["user"])."' AND `pass`='".md5(unsan($_GET["pass"]))."'");
		if(!$result){
			echo("&db_error=".san(mysql_error()));
			exit();
		}
		if(mysql_num_rows($result) != 1){
			$message = "Account does not exist on this universe";
			echo("&error=".san($message));
			exit();
		}
		$row = mysql_fetch_array($result);
		mysql_free_result($result);
		if((isset($row["user"]) and $row["user"] != "") and (isset($row["pass"]) and $row["pass"] != "") and (isset($row["email"]) and $row["email"] != "")){
			$acct_login = true;
			$acct_id = unsan($row["id"]);
			$acct_user = unsan($row["user"]);
			$acct_pass = unsan($row["pass"]);
			$acct_perm = unsan($row["perm"]);
			$acct_email = unsan($row["email"]);
			$acct_active = $row["active"];
			if($row["perm"] == "adm/0"){
				echo("&perm=admin");
			}elseif($row["perm"] == "mod/1"){
				echo("&perm=mod");
			}else{
				echo("&perm=user");
			}
		}else{
			$message = "Account is missing information";
			echo("&error=".san($message));
			exit();
		}
		if((!isset($no_login) or $no_login == false) and (isset($_GET["character"]) and $_GET["character"] != "") and $acct_login == true){
			$result = mysql_query("SELECT `account_id`, `created`, `adate`, `atime`, `mic` FROM `characters` WHERE `id`=".intval(unsan($_GET["character"])));
			if(!$result){
				echo("&db_error=".san(mysql_error()));
				exit();
			}
			if(mysql_num_rows($result) != 1){
				$message = "Character does not exist";
				echo("&error=".san($message));
				exit();
			}
			$row = mysql_fetch_array($result);
			mysql_free_result($result);
			if(unsan($row["account_id"]) != $acct_id){
				$message = "Character does not belong to account \"".san($acct_user)."\"";
				echo("&error=".san($message));
				exit();
			}
			$char_created = $row["created"];
// --------------------------------------------------------------- RELEVANT -------------------------------------------- RELEVANT -----------------------------------------------------------
			if(strpos(strtolower($_SERVER["REQUEST_URI"]), "game.php") !== false){
				$mic = microtime(true);
				$nt = (strtotime(date("m/d/Y")." ".date("H:i:s"))-strtotime($row["adate"]." ".$row["atime"]))+((($mic-floor($mic))*1000-$row["mic"])/1000);
				if($nt*1000 < 250){ //check the execution time against the database's last execution time. If it's less than 250 milliseconds, throw an error
					$no_move = true;
					echo("&s_error=true");
				}
			}
			if(!isset($no_online) or $no_online == false){
				$mic = microtime(true);
				$result = mysql_query("UPDATE `characters` SET `online`=1, `adate`='".date("m/d/Y")."', `atime`='".date("H:i:s")."', `mic`='".(($mic-floor($mic))*1000)."' WHERE `id`=".intval(unsan($_GET["character"])));
				if(!$result){
					echo("&db_error=".san(mysql_error()));
					exit();
				}
			}
// --------------------------------------------------------------- /RELEVANT -------------------------------------------- /RELEVANT -----------------------------------------------------
		}
	}
	
	if(isset($_GET["startuser"])){
		$startuser = intval(decrypt(unsan($_GET["startuser"])));
	}else{
		$startuser = 0;
	}
	$startuser2 = $startuser+100;
	$result = mysql_query("SELECT `id`, `adate`, `atime` FROM `characters` WHERE `online`=1");
	if(!$result){
		echo("&db_error=".san(mysql_error()));
		exit();
	}
	for($i=0;$i<$startuser;$i++){
		$row = mysql_fetch_array($result);
	}
	$nextuser = 1;
	while($row = mysql_fetch_array($result) and $nextuser == 1){
		$start = strtotime($row["adate"]." ".$row["atime"]);
		$end = strtotime(date("m/d/Y")." ".date("H:i:s"));
		$diff = $end-$start;
		if($diff >= 5){
			$result2 = mysql_query("UPDATE `characters` SET `online`=0 WHERE `id`=".$row["id"]);
			if(!$result2){
				echo("&db_error=".san(mysql_error()));
				exit();
			}
		}
		if($startuser > $startuser2){
			$nextuser = 0;
		}
		$startuser++;
	}
	mysql_free_result($result);
	if($nextuser == 1){
		$startuser = 0;
	}
	echo("&startuser=".san(encrypt($startuser)));
}
?>
game.php

Code: Select all

<?php
require("../include/connect.php");
require("../include/sanitize.php");
require("../include/security.php");
require("../include/version.php");
require("../include/ucheck.php");

if($good_connect == true){
	if($acct_login == true){
		if((isset($_GET["keepalive"]) and bol($_GET["keepalive"]) == true) and (isset($_GET["character"]) and $_GET["character"] != "")){
			$result = mysql_query("SELECT * FROM `characters` WHERE `id`=".intval($_GET["character"]));
			if(!$result){
				echo("&db_error=".san(mysql_error()));
				exit();
			}
			$row = mysql_fetch_array($result);
			mysql_free_result($result);
			$result = mysql_query("SELECT `xp` FROM `classes` WHERE `id`=".$row["class"]);
			if(!$result){
				echo("&db_error=".san(mysql_error()));
				exit();
			}
			$row2 = mysql_fetch_array($result);
			mysql_free_result($result);
			$xp_array = explode(",", preg_replace("/\s+/", "", unsan($row2["xp"])));
			echo("&health=".$row["health"]);
			echo("&max_health=".$row["max_health"]);
			echo("&mana=".$row["mana"]);
			echo("&max_mana=".$row["max_mana"]);
			echo("&xp=".$row["xp"]);
			echo("&max_xp=".$xp_array[$row["level"]-1]);
			
			$result = mysql_query("SELECT `name`, `race`, `sex`, `level`, `health`, `max_health`, `mana`, `max_mana`, `speed`, `x`, `y`, `z`, `dir` FROM `characters` WHERE `map`=".$row["map"]." AND `online`=1 AND `id`!=".intval($_GET["character"]));
			if(!$result){
				echo("&db_error=".san(mysql_error()));
				exit();
			}
			$i=0;
			while($row3 = mysql_fetch_array($result)){
				echo("&char".$i."_name=".$row3["name"]);
				echo("&char".$i."_race=".$row3["race"]);
				echo("&char".$i."_sex=".$row3["sex"]);
				echo("&char".$i."_level=".$row3["level"]);
				echo("&char".$i."_health=".$row3["health"]);
				echo("&char".$i."_max_health=".$row3["max_health"]);
				echo("&char".$i."_mana=".$row3["mana"]);
				echo("&char".$i."_max_mana=".$row3["max_mana"]);
				echo("&char".$i."_speed=".$row3["speed"]);
				echo("&char".$i."_x=".$row3["x"]);
				echo("&char".$i."_y=".$row3["y"]);
				echo("&char".$i."_z=".$row3["z"]);
				echo("&char".$i."_dir=".$row3["dir"]);
				$i++;
			}
			echo("&chars=".$i);
			mysql_free_result($result);
			
			if(isset($_GET["init"]) and bol(unsan($_GET["init"])) == true){
				echo("&sprite_x=".$row["x"]);
				echo("&sprite_y=".$row["y"]);
				echo("&sprite_z=".$row["z"]);
				echo("&sprite_dir=".$row["dir"]);
				echo("&init=true");
			}else{
				if((isset($_GET["x"]) and $_GET["x"] != "") and (isset($_GET["y"]) and $_GET["y"] != "") and (isset($_GET["z"]) and $_GET["z"] != "") and (isset($_GET["dir"]) and $_GET["dir"] != "")){
// --------------------------------------------------------------- RELEVANT -------------------------------------------- RELEVANT -----------------------------------------------------------
					$movement = (intval($_GET["x"])+intval($_GET["y"])+intval($_GET["z"]))-($row["x"]+$row["y"]+$row["z"]);
					if($movement < 0){
						$movement *= -1;
					}
					if($movement > ($row["speed"]*30)*0.25){ //the math: character speed*30 FPS*1/4 second = character's max movement speed
													//movement is the player's submitted x, y, and z coords
						echo("&s_error=true");
						$no_move = true;
					}
					if(intval($_GET["dir"]) == 1 or intval($_GET["dir"]) == 5){
						$dir = 0;
					}elseif(intval($_GET["dir"]) == 2 or intval($_GET["dir"]) == 6){
						$dir = 1;
					}elseif(intval($_GET["dir"]) == 3 or intval($_GET["dir"]) == 7){
						$dir = 2;
					}else{
						$dir = 3;
					}
					if(isset($no_move) and $no_move == true){
						echo("&sprite_x=".$row["x"]);
						echo("&sprite_y=".$row["y"]);
						echo("&sprite_z=".$row["z"]);
					}else{
						$result = mysql_query("UPDATE `characters` SET `x`=".intval($_GET["x"]).", `y`=".intval($_GET["y"]).", `z`=".intval($_GET["z"]).", `dir`='".$dir."' WHERE `id`=".intval($_GET["character"]));
						if(!$result){
							echo("&db_error=".san(mysql_error()));
							exit();
						}
						echo("&sprite_x=".intval($_GET["x"]));
						echo("&sprite_y=".intval($_GET["y"]));
						echo("&sprite_z=".intval($_GET["z"]));
					}
// --------------------------------------------------------------- /RELEVANT -------------------------------------------- /RELEVANT -------------------------------------------------------------
				}else{
					$message = "Required field(s) missing";
					echo("&error=".san($message));
					exit();
				}
			}
		}else{
			$message = "Required field(s) missing";
			echo("&error=".san($message));
			exit();
		}
	}else{
		$message = "Not logged in";
		echo("&error=".san($message));
		exit();
	}
}
?>
In short: If anyone would like to put in their two cents, it would be very much appreciated :D

And if interested, I set up a test account. (The server is in Germany. Expect a lot of latency over an ocean)
http://gametack.org/download/tactics2.html (no worries, it's an in-browser game)
user: test
pass: test
User avatar
twinedev
Forum Regular
Posts: 984
Joined: Tue Sep 28, 2010 11:41 am
Location: Columbus, Ohio

Re: An interesting problem with a PHP-based game

Post by twinedev »

I went to try it and got told it was an invalid login.

My big concern in the way you describe your setup is that EACH play will be doing 4 calls per second to the server. Keep in mind the I/O load, not only do you just have the basic call to the server and passing back data, you also have database connections and queries each call, so you need to make sure you have all the databases indexed and optimized for best performance, same for the actual SQL. (ex, are you really needing every field when you do SELECT * )

Also another thing to consider, log files are also more I/O. Running this through apache, using default settings, each call will be around 150 bytes, 10 people playing for an hour is 360k in generated log files.

Sorry didn't get you the answer you want, just the general description of use reminded me of some of the same things that give problems with doing an online chat system running through LAMP instead of a custom service through a dedicated port.

When you get the login working though, let me know, I'd still like to check it out.

-Greg
User avatar
twinedev
Forum Regular
Posts: 984
Joined: Tue Sep 28, 2010 11:41 am
Location: Columbus, Ohio

Re: An interesting problem with a PHP-based game

Post by twinedev »

PS, just for fun, I ran AB on the URL you gave, did 1000 requests, and it came back with results that it handled 148 requests per second, which when you break that down to 4 per seconds per user, that means it will max out and start lagging at 37 users at once.

-Greg
User avatar
egg82
Forum Contributor
Posts: 156
Joined: Sat Oct 01, 2011 9:29 pm
Location: Colorado, USA

Re: An interesting problem with a PHP-based game

Post by egg82 »

What's interesting is that it works when you hit the enter key instead of using the login button. Must remember to fix that.

Anyway, I thought about that when I started the project as well. I made it so all I have to do it copy two folders and import an sql file to create a new server.
My hope is that if it does catch on, i'll be able to spread the load out to many servers. If it doesn't, I won't have much to worry about as far as server load.
I have my own logging method, so as far as reading apache logs, I hopefully won't have too much to worry about.

Edit: Nice find! Though I split the servers like this:
the application connects to a "main" server to get core data and other servers. After that, you select a server to use.
As GT is located in Germany, I will be using it as the main and private testing server as well as the application download server. This will drastically cut the load.

Oh, and more than one person can be on the same account at once. Just not the same character.
You can use the test account and create two characters. They will be able to see eachother, so you can get a feel for the latency.
User avatar
egg82
Forum Contributor
Posts: 156
Joined: Sat Oct 01, 2011 9:29 pm
Location: Colorado, USA

Re: An interesting problem with a PHP-based game

Post by egg82 »

Out of curiosity, what application did you use to run the test?

Side note: I'm re-optimizing both client and server-side scripts for speed and functionality. I might as well run a few tests on the URLs
Post Reply