InSim PHP5-Tutorial

From LFS Manual
Revision as of 14:02, 5 February 2010 by Dygear (talk | contribs) (Added information.)
Jump to navigationJump to search

This tutorial will only work if you or your webhoster have PHP version 5.0.0 or higher installed (5.1.x recommended!), because the stream_socket_*-functions are not available in PHP 4 and the sockets that can be created in PHP 4 using the socket_*-functions can not be run really non-blocking, so you can not prevent errors like timeouts.

WARNING: This tutorial will only work with the OLD InSim provided up to patch W!
A new version of this article is expected to come soon.

Datatypes

The Data types that are mentioned in your docs\InSim.txt file are C/C++ Datatypes and have to be converted to PHP datatypes when you read packages and back to C/C++ Datatypes when you want to send a package to InSim. The PHP-functions pack(), unpack() and str_pad() will help us converting.

char

<?php
// if a part of a package has the datatype "char", it is simply plain text,
// but str_pad() MUST be used to make this part exactly as long as it is told in InSim.txt.
// 
// Example: InSim.txt says the part "Admin" of the InSimInit packet has the datatype char[16],
// but the admin-password of your server is only 8 characters long. So we have to str_pad it:
$adminpw_padded = str_pad($adminpw, 16, "\0");

// char-data must not be longer than it is specified in InSim.txt. If someone would specify
// an admin-password that is 17 chars long or longer, f.e. $adminpw = "myverylongadminpw";
// we would have to cut it down to 16 chars BEFORE str_padding it.
// (logging in to the LfS-Servers InSim-port would work flawlessly because the server
//  also only allows the admin-pw to be 16 chars long.)
if (strlen($adminpw) > 16) {
  $adminpw = substr($adminpw, 0, 16);
}
?>

byte

<?php
// Parts of Packages that have the datatype "byte" are int-variables in PHP and are converted
// using pack() and unpack():
$byte_packed = pack("c", $byte);

$temp = unpack("c",$byte_packed);
$byte_unpacked = $temp[1];
?>

word

<?php
// Parts of Packages that have the datatype "word" are strings/ints in PHP and are converted
// using pack() and unpack():
$word_packed = pack("S", $word);

// "words" that work properly with unpack must be atleast 2 chars long, so pad it with "\0"
if(strlen($word_packed) < 2) {
  $word_packed = str_pad($word_packed, 2, "\0");
}
$temp = unpack("S",$word_packed);
$word_unpacked = $temp[1];
?>

short

<?php
// Parts of Packages that have the datatype "short" are int-variables in PHP and are converted
// using pack() and unpack():
$short_packed = pack("s", $short);

$temp = unpack("s",$short_packed);
$short_unpacked = $temp[1];
?>

unsigned

<?php
// Parts of Packages that have the datatype "short" are int-variables in PHP and are converted
// using pack() and unpack():
$unsigned_packed = pack("I", intval($int)); //intval used to ensure datatype is int

$temp = unpack("I",$int_packed);
$unsigned_unpacked = $temp[1];
?>

int

<?php
// Parts of Packages that have the datatype "int" are int-variables in PHP and are converted
// using pack() and unpack():
$int_packed = pack("i", intval($int)); //intval used to ensure datatype is int

$temp = unpack("i",$int_packed);
$int_unpacked = $temp[1];
?>

float

<?php
// Parts of Packages that have the datatype "byte" are float-variables in PHP and are converted
// using pack() and unpack():
$float_packed = pack("f", $float);

$temp = unpack("f",$float_packed);
$float_unpacked = $temp[1];
?>

MSHT

MSHT is a time-information (Minutes Seconds Hundreths Thousandths) and simply consists of 4 byte-type parts.

How to build/parse packets

Assembling packets

To assemble a packet, simply put the single pieces together using the string concatenation operator. We will use the ISI (InSimInit) packet as example here.


<?php
$adminpw = "adminpwd";
$packet  = "";
$packet .= "ISI\0";                     // Packet-ID
$packet .= pack("S", 30000);            // 30000 as example response port
$packet .= pack("c", 2+4+32);           // Connection Flags - see InSim.txt
$packet .= pack("c", 1);                // NodeSecs - time between packages
if (strlen($adminpw) > 16) {
  $adminpw = substr($adminpw, 0, 16);   // Cut down adminpw if too long
}
$packet .= str_pad($adminpw, 16, "\0"); // Admin-Password if set in LFS host options
?>

Now you could send the packet to InSim.

Disassembling packets

To disassemble a packet, you can simply use substr() and then unpack the data. We will use a simple VER (InSimVersion)-packet as example here.

<?php
$packet  = receiveVER(); // The function does not exist, just used to make the ex. more readable
$lfsVersion = trim(substr($packet,  4, 8)); // char
$lfsProduct = trim(substr($packet, 12, 6)); // char
$isv_raw = substr($packet, 18, 2);          // word
if(strlen($isv_raw) < 2) {
  $isv_raw = str_pad($isv_raw, 2, "\0");
}
$temp = unpack("S",$isv_raw);
$insimVersion = $temp[1];
?>

Or you could do it in one fowl swoop, and out put an array.

<?php
$packet = unpack('a4VER\A8Version\A6Product\SInSimVer', receiveVER());
print_r($packet);
?>

This could output ...

Array (
    'VER' => 'VER ',
    'Version' => '0.5V',
    'Product' => 'DEMO',
    'InSimVer' => '1 '
)

Connecting to InSim

To connect to InSim, we need to open 2 socket-streams: one that sends data to InSim, and one that receives the answers from InSim. We will use fsockopen() for the sender connection and stream_socket_server() for the receiver.

<?php
// CONFIG START
$insimIP = '127.0.0.1'; // Your InSim-IP Here
$insimPort = 29999;     // Your InSim-Port
$adminPW = 'adminpwd';  // Your Admin-Password
// CONFIG END

// create sender filestream
$errno = 0; $errstr = "";
$sender = @fsockopen("udp://$insimIP", $insimPort, &$errno, &$errstr, 3);
if (!$sender) {
  die("Error:\nCould not connect to $insimIP:$insimPort\n"

      . "Error Number: $errno\nError Description: $errstr");
}

// create receiver filestream
$localport = 30000;
$receiver = false;
while ($localport <= 65535) {
  $receiver = @stream_socket_server("udp://0.0.0.0:$localport", $errno, $errstr, STREAM_SERVER_BIND);
  if (is_resource($receiver)) {
    break;
  }
  $localport++;
  $receiver = false;
}
if ($receiver === false) {
  die("Error:\nCould not bind to $localport\nError Number: $errno\nError Description: $errstr");
}

// Make the receiver stream nonblocking to be able to apply timeouts
stream_set_blocking($receiver, 0);

/*
  VARIABLES AFTER HERE:
  $sender    : sender   filestream
  $receiver  : receiver filestream
  $localport : port of receiver filestream
  + config variables
*/

// We will now have to send an ISI (InSimInit)-packet to InSim to make it accept our requests.
// Prepare packet
$packet  = "";
$packet .= "ISI\0";                        // Packet-ID
$packet .= pack("S", $localport);          // response port
$packet .= pack("c", 2+4+32);              // Connection Flags - see InSim.txt
$packet .= pack("c", 1);                   // NodeSecs - time between packages
if (strlen($adminPW) > 16) {
  $adminPW = substr($adminPW, 0, 16);      // Cut down adminpw if too long
}
$packet .= str_pad($adminPW, 16, "\0");    // Admin-Password if set in LFS host options
// Send packet
fwrite($sender, $packet, strlen($packet)); // Third parameter to make PHP ignore magic_quotes-setting
?>

If everything works, LfS should display a message like "InSimInit: Port 30000".

Disconnecting from InSim

Disconnecting from InSim is a lot easier. We have to send an InSimPack with "ISC" as ID to tell InSim we won't send any more packages and then we can close the sender and receiver filestreams.

<?php
$packet = "ISC\0" . pack("i", intval(0));
fwrite($sender, $packet, strlen($packet));
fclose($sender);
fclose($receiver);
?>

LfS will display "InSimClose: Waiting for connection".

Easy example

In this example, we will connect to InSim, receive Version-Informations of LFS and the LFS hostname, and then we will send a text message to the server and force it to reinitialize. I will assume that you have copied the script-piece from "Connecting to InSim" to a file called "connect.inc.php" (and of course updated the configuration part so it fits to your LfS-server), and that you have copied the script-piece from "Disconnecting from InSim" to a file called "disconnect.inc.php". This will make the example more readable and will prevent too much useless (because doubled) code here.


<?php
// Output will be plain text
header("Content-Type: text/plain; Charset=ISO-8859-1");
// Connect to InSim
require "connect.inc.php";
echo "Connected! Requesting Version packet...\n\n";

// The version-request packet is a simple InSimPack with ID "VER" and Value 0.
$packet = "VER\0" . pack("i", intval(0));

// send packet
fwrite($sender, $packet, strlen($packet));

$packet = false;
// receive answer from LfS: a packet with ID "VER"

$timeout = time() + 2;
while (!$packet && time() <= $timeout) {
  if ($packet = fread($receiver, 256)) {
    break;
  }
}

// check if really a version-packet arrived or something else we cant deal with at the moment
if (!$packet || substr($packet, 0, 3) != "VER") {
  echo "No version package arrived, sorry :-(\n\n";
}
else {
  // Parse version-package
  $lfsV = trim(substr($packet,  4, 8)); // char
  $lfsP = trim(substr($packet, 12, 6)); // char
  $isv_raw = substr($packet, 18, 2);          // word
  if(strlen($isv_raw) < 2) {
    $isv_raw = str_pad($isv_raw, 2, "\0");
  }
  $temp = unpack("S",$isv_raw);
  $insimV = $temp[1];
  
  // Show some version-information
  echo "LfS Version-information:\n  LfS Product: $lfsP\n"
     . "  LfS Version: $lfsV\n  InSim Version: $insimV\n\n";
}

echo "Requesting InSimMulti-Package with Hostname now...\n\n";

// Now we request an InSimMulti-Package to get the LfS Hostname (if LfS is in multiplayer mode)
// To perform the request, we simply send an InSimPack with ID = "ISM" and Value = 0.
$packet = "ISM\0" . pack("i", intval(0));

// send packet
fwrite($sender, $packet, strlen($packet));

$packet = false;
// receive answer from LfS: a packet with ID "ISM"

$timeout = time() + 2;
while (!$packet && time() <= $timeout) {
  if ($packet = fread($receiver, 256)) {
    break;
  }
}


// check if really a ISM-packet arrived or something else we cant deal with at the moment
if (!$packet || substr($packet, 0, 3) != "ISM") {
  echo "No InSimMulti-package arrived, sorry :-(\n\n";
}

// Get LfS connection type: are we connected to a client (0) or a server (1)?
$type_raw = substr($packet, 4, 1);
$temp = unpack("c", $type_raw);
$type = $temp[1];

echo "LfS type: " . (($type == 0)?"Client":"Server") . "\n\n";

// Get LfS Hostname
$hostname = trim(substr($packet, 8, 32));
if(empty($hostname)) {
  echo "Not in multiplayer mode\n\n";
}
else {
  echo "Hostname: $hostname";
}

// if we are connected to a server, restart it
if($type == 1) {
  echo "Now forcing server restart...\n\n";

  // To inform the users, we will first send a MST(MessageTypePack)-packet to the server containing
  // the information that the server will be restarted now.
  // then we will force a server reinit, also using a MST.
  $packet = "MST\0" . str_pad("^1SORRY, BUT THE SERVER WILL BE RESTARTED IN 3 SECONDS", 64, "\0");
  fwrite($sender, $packet, strlen($packet));
  $packet = "MST\0" . str_pad("/reinit", 64, "\0");
  fwrite($sender, $packet, strlen($packet));
}

// Disconnect from InSim
require "disconnect.inc.php";
?>

LfS should display the following (if we were connected to a host):

InSimInit: port 30000
host: SORRY, BUT THE SERVER WILL BE RESTARTED IN 3 SECONDS
/reinit
InSimClose: waiting for connection
Host will restart in 3 seconds
Track loaded

If we were connected to a client, LfS should display:

InSimInit: port 30000
InSimClose: waiting for connection