Switch branches/tags
Nothing to show
Find file
Fetching contributors…
Cannot retrieve contributors at this time
executable file 553 lines (445 sloc) 15.5 KB
Hennepin requires PHP to have been built with --enable-sockets
Just read through the Configuration section to run Hennepin
CVS info: $Id: hennepin.php,v 1.46 2004/12/14 18:19:59 malaprop Exp $
// Configuration ///////////////////////////////////////////
// IP address to listen on, for all
// $IP=''; // commented out for socket_create_listen (below)
// port to listen on
// "WARN" comments note that PHP warnings can be removed by uncommenting the
// following code. As they're generally harmless, the bytes were instead saved.
// Also, depending on what error_reporting is set to, PHP may get real spammy
// with how I've played fast-and-loose with syntax. You may wish to set:
// error_reporting = E_ALL & ~E_NOTICE
// in your php.ini, or uncomment:
// error_reporting(E_ALL ^ E_NOTICE);
// to start Hennepin, make the first line of this file the same as
// `which php` -- it'll probably be /usr/bin/php or /usr/local/bin/php
// then: ./hennepin
// When coding, smashing global vars with uppercase names will either
// 1. crash the mud or 2. introduce subtle errors that will much later cause #1
// So be careful with them - they're documented for a reason!
// Data Structures /////////////////////////////////////////
/* $C -- connections, an array of the connected users
Every connection is represented by an array in $C. Upon connection, they're
indexed by socket id # (which is not a valid socket resource, because it's a
key and not a value). As soon as they provide a name, the entry is replaced by
one with their name to save the hassle of looking them up all the time.
* Networking includes an [ip] for their ip address, and [s] for socket
* [ib] is their input buffer, it should not be manipulated except by the
existing network code
* [in] points to the function that get the next line of input, w/o \n. See:
* [name], [pwd], and other variables are game info. [l] is last command
* [b] is whether they're busy doing something -- so any commands issued will be delayed
* [hp] hit points, [vit] vitality, [str] strength, [ste] stealth, [per] perception
/* $U -- load up user db
$U has the same structures as $C and is the store for players.
When a player is loaded, their old [ip] and [s] need to be removed first.
[a] is the area they're in; [r] is the room; [c] is if they support ansi
/* $W -- the world data
first-level index is network id, which has these keys:
* [name] -- name of network owner
* [n] -- array of the nodes, indexed by [name]
* [type] -- the type of node
* [vit] -- vitality of the node
* [hp] -- hp of the node
* [c] -- array of connections between nodes, indexed by [name]
* [f] -- node id from
* [t] -- node id to
* [bi] -- boolean for bidirectionality
* [ste] -- stealth of the node
/* $V -- an array of verbs
Hennepin can load code at runtime, creating verbs
$V is an array of callable functions indexed by name
No need to explicitly declare it, so we'll save the bytes
// $D -- current working directory
// Functions ///////////////////////////////////////////////
/* w -- short for socket_write, sends a message to one connection
Because I can't seem to get character mode, it adds a \n for you.
Colors: do 0n for dark and 1n for light, n may be:
0 - black (useless, really)
1 - red
2 - green
3 - yellow
4 - blue
5 - magenta
6 - cyan
7 - gray
function w($s,$m){
// do ansi color replacements and
// linebreak (PHP default is 75, and that's good enough)
// a -- announce a string to all players
function a($m){
foreach($C as$s=>$p)w($s,$m); // just loop over everybody
// b -- send a string to all in a room but the player
function b($s,$m){
foreach($C as$n=>$p){
// if not the player, and in the same area and room
w($n,$m); // then give them the message
// n -- network say -- send a message to people on the same network
function n($s,$m){
foreach($C as$n=>$p){
w($n,$m); // then give them the message
// d -- disconnect socket and save player
function d($s){
$n=$C[$s][name]; // save their name for printing
$U[$n]=$C[$s]; // save their character to $U
l("disc: {$C[$s][ip]} $n"); // log the event
socket_shutdown($C[$s][s]); // clean up network state
unset($C[$s]); // remove them from connections
su(); // save the userfile
if($n)a("~12$n quit"); // if they had logged in, announce departure
// l -- timestamped log to console
function l($m){echo date('Y-m-d H:i:s ')."$m\n";}
/* h -- halt Hennepin, PHP will clean up sockets for us
The socket will (annoyingly) sit in TIME_WAIT for 60 seconds before you can restart Hennepin
function h(){
// save our data
// call it a night
// s -- search $C for a user named $n, return their socket
function s($n){
// search all connections, checking for a matching name
foreach($C as$s=>$v)
return $C[$s][s];
// if no connection is found, return false
return 0;
// f -- serialize second arg and save it to a file
function f($f,$a){
$r=fopen("$D/$f",'w'); // shorter to put the slash here, once
l("saved $f");
// If using PHP5, comment out above block and:
// file_put_contents($f,serialize($a));
// i -- input a file, unserialize it, and return it
function i($f){
/* r -- registers a verb
$a is an array holding info on it: first line is name, rest is code
you may notice in verbs that the second line is '// <?': this is just for vim highlighting
bytesaving: all verbs get started with 'global$C,$U,$V,$W;'
all verbs get called with:
$s -- socket of player
$d -- whether this is a delayed call
$r -- rest of input after command
function r($a){
// create the function for each of its possible names
// if $a came directly out of a file (eg, on startup) there'll be a \n to trim off $a[0]
foreach($n as$i)
// lt -- long time, because time() returns only seconds
// it returns a string with lots of precision, the change to float will lose
// all but hundreths
function lt(){
$t=explode(' ',microtime());
// PHP floats are only so long -- this gives us two more digits
// after the decimal
/* t -- registers a tick call
$f -- name of function to call -- if it starts with $, that verb will be called
$a -- argument to pass: Can't pass more than one, but $a can be an array
$w -- how many seconds to call it in
$s -- optional, the user's socket for delayed verb calls
function t($f,$a,$w,$s=0){
$T[]=array(lt()+$w,$f,$a,$s); // fewer bytes vs. proper subscript labels
// dt -- do ticks that are due or overdue
function dt(){
// go through all the pending ticks
foreach($T as $e=>$f)
if($f[0]<=lt()){ // if this entry needs to be done
$V[substr($f[1],1)]($f[3],1,$f[2]); // call the verb
$f[1]($f[2]); // do it
unset($T[$e]); // and call it done
// su -- save users, if $t, we were called by tick, so set up the next one
function su($t=0){
f('users',$U); // just farm it out to the right function
// sw -- save world; if $t, we were called by tick, so set up the next one
function sw($t=0){
f('world',$W); // just farm it out to the right function
// gn -- generate a network recursively
function gn($n){
global $W;
// do
/* $W -- the world data
first-level index is network id, which has these keys:
* [name] -- name of network owner
* [n] -- array of the nodes, indexed by [name]
* [type] -- the type of node
* [vit] -- vitality of the node
* [hp] -- hp of the node
* [c] -- array of connections between nodes, indexed by [name]
* [f] -- node id from
* [t] -- node id to
* [bi] -- boolean for bidirectionality
* [ste] -- stealth of the node
// Input-handling functions /////////////////////////////////////////
/* every input-handling function gets one line at a time and has two args:
$s for what connection in $C it came from
$m is the trim()ed line of input (so no leading/trailing whitespace/newlines)
// get their name
function name($s,$m){
// pass off new users to newu()
if($m=='new'){w($s,"Desired name:");$C[$s][in]='newu';return;}
if($U[$m]&&!$C[s($m)]) {
$C[$s]=array_merge($C[$s],$U[$m]); // load their info
$C[$s][in]='pwd'; // pass control to pwd()
w($s,"Bad name, try again:");
// new user, get and save their name
function newu($s,$m){
// only alphabetic names allowed
// no blank or existing names allowed
if(!$m||$U[$m]){w($s,"Bad name, try again:");return;}
$C[$s][name]=$m; // save their name
$C[$s][l]="A plain user."; // give them a generic description
//$c[$s][c]=0; // default ansi off
$C[$s][cash]=0; // start with $0
$C[$s][in]='pwd'; // get their password
// if there are no users saved yet, we've just started up
// so lucky user #1 gets to be wiz
// check/set password
function pwd($s,$m){
if($m==$C[$s][pwd]|| // can login with a matching pwd
($C[$s][pwd]==""&&$m&&$C[$s][pwd]=$m)){ // if blank pwd space, save new user password
a("~12{$C[$s][name]} logs into Hennepin"); // tell everyone
l("login: {$C[$s][ip]} {$C[$s][name]}"); // log it
$C[$s][a]='root'; // send them home
// default stats
w($s,"Type 'commands'"); // give instructions
$C[$s][in]='game'; // pass control to game()
$U[$C[$s][name]]=$C[$s]; // save their character to $U
w($s,"Bad password, try again:"); // yeah, I'm way too nice
// main game input function
function game($s,$m){
global$C,$V,$U,$T,$W; // $U,$T are here for the benefit of eval()d code
// log the input
l($C[$s][name].": $m");
// parse $m into command $o and rest $r
if($pos=strpos($m,' ')){
// handle the case with no arguments, leave $r unset
// If they're a wizard, they can eval() -- execute raw PHP. Powerful stuff.
eval("$r;"); // I always forget the ;, it's worth 3 bytes
// if they called a verb by full name
$V[$o]($s,0,$r); // all verbs get these args
// or if they did an abbreviation like ' for say
// if they're typing the name of an exit, pass off to go
// just ignore blank entries
// if we don't know *what* they want, react intelligently...
// set the last command -- is down here so that the ! command works
// Give them a prompt with area and room if they're still here
if($C[$s])socket_write($C[$s][s],"{$C[$s][a]}/{$C[$s][r]} {$C[$s][hp]}> ");
// Gameplay functions //////////////////////////////////////
/* gc -- get all connections from the given area/room
function gc($a,$r){
$v=array(); // conns to return -- the spam is very painful without this
foreach($W[$a][c] as$n=>$c)
/* gnc -- get named connection for the given area/room
function gnc($a,$r,$n){
/* rd -- roll $n d6
function rd($n){
rsort($a); // prep for cr()
/* cr -- compare a rolls to d rolls for success
Positive numbers are in favor of attacker, negative in favor of defender, 0 is tied
function cr($a,$d){
// roll the dice
// cut down size of attacker if defender is smaller
// loop and count successes
foreach($a as$i=>$v)
// Startup /////////////////////////////////////////////////
// By default, PHP stops scripts after 30 seconds
// Turn off PHP's output buffering
// create the resource socket -- socket_create_listen saves so many bytes!
// PHP's default error messages are informative enough if something breaks here
$RS=socket_create_listen($P)or die();
socket_set_nonblock($RS)or die();
// start saving users and world every 5 minutes
// load up all the saved verbs and register them
// When . and .. are read, file() returns an empty array; r() will be OK
while($f=readdir($r)) // no check for !== false, no dir in /verbs is named null, 0, or false
ksort($V); // not needed, but makes for nicer display from 'commands'
// PHP will close the dir for us when we exit, amount of ram/handles wasted are trivial
// Game Loop ///////////////////////////////////////////////
// We'll die() when done and cleaned up (see h()), so this loop is infinite
// check for activity, with timeout (1/10th of a second) to deal with game tasks
$r=array($RS); // $rs is included to listen for new connections
foreach($C as$p){array_push($r,$p[s]);} // and load all the players' sockets
$a=socket_select($r,$w=NULL,$e=NULL,0,100000); // $a is the number of sockets to pay attention to
// Deal with socket errors by dumping them to console, and soldiering on
// deal with any network happenings
// accept new connections / read incoming data
foreach($r as$s) {
// if it's a new connection, and we accept it, save it
socket_getpeername($n,$C[$n][ip]); // get their IP
$C[$n][s]=$n; // resource ID can't be a key
$C[$n][in]='name'; // name() starts the login process
w($n, "Welcome to Hennepin, enter your name or: new");
l("conn: {$C[$n][ip]}");
}else{ // input to take
if(!$i=socket_read($s,99)) // lost connection
else // add it to their input buffer
// maybe do output buffering with $w here?
// it's not likely to be a problem, so I'll save the bytes
// game functioning
// process input -- loop over all players and deal with their buffers
foreach($C as$s=>$p){
if(!$p[ib])continue; // nothing to do
$p[ib]=str_replace("\r",'',$p[ib]); // no \r's! rar!
// if there's a whole line of input, deal with it
$p[in]($p[s],trim(substr($p[ib],0,$pos))); // send to the func. that gets their input
if($C[$s])$C[$s][ib]=substr($C[$s][ib],$pos+2); // if they're still here, trim the buffer -- use $C so the ! command can dick with the buffer
// time to process any pending ticks
} // end of the game's infinite while() loop