Acme-MUDLike
view release on metacpan or search on metacpan
lib/Acme/MUDLike.pm view on Meta::CPAN
# Todo:
#
# * what would be *really* cool is doing on the fly image generation to draw an overhead map of the program based on a
# graph of which objects reference which other objects and let people go walk around inside of their program
# and then they could fight methods and use global variables as weapons!
#
# * http://zvtm.sourceforge.net/zgrviewer.html or something similar for showing the user the "map" of
# nodes/rooms/whatever made of has-a references or something.
#
# * /goto should put you inside an arbitrary object, /look should list as exits and/or items the object references contained by that object
# in other words, break away from our rigid API for inventory/room/etc.
#
# * need a black list black list, so we can re-add ourself to things that get serialized by Acme::State even though we're in %INC
#
# * need an error log viewabe by all.
#
# * eval and its output should be sent to the whole room.
#
# * Better account management.
#
# * There's code around to parse LPC and convert it to Perl. It would be neat to offer a full blown 2.4.5
# lib for people to play around in.
#
# * Acme::IRCLike would probably be more popular -- bolt an IRC server onto your app.
#
# * Also, a telnet interface beyond just an HTTP interface would be nice. Should be easy to do.
#
# * Let "players" wander between apps. Offer RPC to support this.
#
# * Optionally take an existing Continuity instance with path_session set and optionally parameters
# for the paths to use for chat pull and commands.
# Not sure how to work this; each path gets its own coroutine, but there is still only one main().
# Continuity doesn't have a registry of which paths go to which callbacks.
#
# Done:
#
# * mark/call commands should have a current object register, so you can do /call thingie whatever /next and then be calling
# into the object returned by thingie->whatever
#
# * /list (like look, but with stringified object references)
#
# * /mark <n> ... or... /mark <stringified obj ref>
#
# * messages still in duplicate when the same player logs in twice; make room's tell_object operate uniquely.
#
# * messages in triplicate because each player has three routines and is inserted into the floor three times. oops.
#
# * build the ajax.chat.js into source. -- okay, test.
#
# * eval, call
#
# * inventory's insert() method should set the insertee's environment to itself. that way, all objects have an environment.
#
# * Commands need to do $floor->tell_object or $self->tell_object rather than output directly.
#
# * Put @messages into the room ($floor). Get the chat action out of the main loop. Dispatch all
# actions. Maybe.
#
our $password; # Acme::State friendly
our $floor; # holds all other objects
our $players; # holds all players; kind of like $floor except in the future, inactive players might get removed from the floor, or there might be multiple rooms
my $continuity;
my $got_message; # diddled to wake the chat event watchers
$SIG{PIPE} = 'IGNORE';
sub new {
my $package = shift;
my %args = @_;
die "We've already got one" if $continuity;
$password = delete $args{password} if exists $args{password};
$password ||= join('', map { $_->[int rand scalar @$_] } (['a'..'z', 'A'..'Z', '0'..'9']) x 8),
my $staticp = sub {
# warn "staticp: url->path: ``@{[ $_[0]->url->path ]}''";
return 0 if $_[0]->url->path =~ m/\.js$/;
# warn "staticp: dynamic js handling override not engaged";
return $_[0]->url->path =~ m/\.(jpg|jpeg|gif|png|css|ico|js)$/
};
$continuity = $args{continuity} || Continuity->new(
staticp => sub { $staticp->(@_); },
callback => sub { login(@_) },
path_session => 1,
port => 2000,
%args,
);
print "Admin:\n", $continuity->adapter->daemon->url, '?admin=', $password, '&nick=', (getpwuid $<)[0], "\n";
$floor ||= Acme::MUDLike::room->new();
$players ||= Acme::MUDLike::inventory->new();
bless { }, $package;
}
sub loop { my $self = shift; $continuity->loop(@_); }
sub header {
qq{
<html><head>
<script src="/jquery.js" type="text/javascript"></script>
<script src="/chat.js" type="text/javascript"></script>
</head><body>
};
}
sub footer { qq{</body></html>\n}; }
sub login {
my $request = shift;
#
# per-user variables
#
my $player;
# STDERR->print("debug: " . $request->request->url->path . "\n"); # XXX
# STDERR->print("debug: " . $request->request->as_string . "\n"); # XXX
$SIG{PIPE} = 'IGNORE'; # XXX not helping at all. grr.
#
# static files
#
if($request->request->url->path eq '/chat.js') {
# warn "handling chat.js XXX: ". $request->request->url->path;
$request->print(Acme::MUDLike::data->chat_js());
return;
} elsif($request->request->url->path eq '/jquery.js') {
# warn "handling jquery.js XXX: ". $request->request->url->path;
$request->print(Acme::MUDLike::data->jquery());
return;
}
#
# login
#
while(1) {
my $nick_tmp = $request->param('nick');
my $admin_tmp = $request->param('admin');
if(defined($nick_tmp) and defined($admin_tmp) and $nick_tmp =~ m/^[a-z]{2,20}$/i and $admin_tmp eq $password) {
my $nick = $nick_tmp;
$player = $players->named($nick) || $players->insert(Acme::MUDLike::player->new(name => $nick), );
$player->request = $request;
# @_ = ($player, $request,); goto &{Acme::MUDLike::player->can('command')};
$player->command($request); # doesn't return
}
# warn "trying login again XXX";
$nick_tmp ||= ''; $admin_tmp ||= '';
$nick_tmp =~ s/[^a-z]//gi; $admin_tmp =~ s/[^a-z0-9]//gi;
$request->print(
header, # $msg,
qq{
<form method="post" action="/">
<input type="text" name="nick" value="$nick_tmp"> <-- nickname<br>
<input type="password" name="admin" value="$admin_tmp"> <-- admin password<br>
<input type="submit" value="Enter"><br>
</form>
},
footer,
);
$request->next();
}
}
#
# object
#
package Acme::MUDLike::object;
sub new { my $package = shift; bless { @_ }, $package; }
sub name :lvalue { $_[0]->{name} }
sub environment :lvalue { $_[0]->{environment} }
sub use { }
sub player { 0 }
sub desc { }
sub tell_object { }
sub get { 1 } # may be picked up
sub id { 0 }
#
# inventory
#
package Acme::MUDLike::inventory;
sub new {
# subclass this to build little container classes or create instances of it directly
my $package = shift; bless [ ], $package;
}
sub delete {
my $self = shift;
my $name = shift;
for my $i (0..$#$self) {
return splice @$self, $i, 1, () if $self->[$i]->id($name);
}
}
sub insert {
my $self = shift;
my $ob = shift;
UNIVERSAL::isa($ob, 'Acme::MUDLike::object') or Carp::confess('lit: ' . $ob . ' ref: ' . ref($ob));
push @$self, $ob;
$ob->environment = $self;
$ob;
}
sub named {
my $self = shift;
my $name = shift;
for my $i (@$self) {
return $i if $i->id($name);
}
}
sub apply {
lib/Acme/MUDLike.pm view on Meta::CPAN
my $func = shift;
my @args = @_;
my @ret;
for my $i (@$self) {
if(ref($func) eq 'CODE') {
push @ret, $func->($i, @args);
} else {
push @ret, $i->can($func)->($i, @args);
}
}
return @ret;
}
sub contents {
my $self = shift;
return @$self;
}
#
# room
#
package Acme::MUDLike::room;
push our @ISA, 'Acme::MUDLike::inventory';
sub tell_object {
my $self = shift;
my $message = shift;
# rather than buffering messages, room objects recurse and distribute the message to everyone and everything in it
# $self->apply('tell_object', $message);
my %already_told;
$self->apply(sub { return if $already_told{$_[0]}++; $_[0]->tell_object($message); }, );
}
#
# players
#
package Acme::MUDLike::players;
push our @ISA, 'Acme::MUDLike::inventory'; # use base 'Acme::MUDLike::inventory';
#
# player
#
package Acme::MUDLike::player;
push our @ISA, 'Acme::MUDLike::object';
sub player { 1 }
sub new {
my $pack = shift;
bless {
inventory => Acme::MUDLike::inventory->new,
messages => [ ],
@_,
}, $pack;
}
sub request :lvalue { $_[0]->{request} }
sub id { $_[0]->{name} eq $_[1] or $_[0] eq $_[1] }
sub name { $_[0]->{name} }
sub password { $_[0]->{password} }
sub x :lvalue { $_[0]->{x} }
sub y :lvalue { $_[0]->{y} }
sub xy { $_[0]->{x}, $_[0]->{y} }
sub get { 0; } # can't be picked up
sub inventory { $_[0]->{inventory} }
sub evalcode :lvalue { $_[0]->{evalcode } }
sub current_item :lvalue { $_[0]->{current_item} }
sub tell_object {
my $self = shift;
my $msg = shift;
push @{$self->{messages}}, $msg;
shift @{$self->{messages}} if @{$self->{messages}} > 100;
$got_message = 1; # XXX wish this didn't happen for each player but only once after all players got their message
}
sub get_html_messages {
my $self = shift;
return join "<br>\n", map { s{<}{\<}gs; s{\n}{<br>\n}g; $_ } $self->get_messages;
}
sub get_messages {
my $self = shift;
my @ret;
# this is written out long because I keep changing it around
for my $i (1..20) {
exists $self->{messages}->[-$i] or last;
my $msg = $self->{messages}->[-$i];
push @ret, $msg;
}
return reverse @ret;
}
sub header () { Acme::MUDLike::header() }
sub footer () { Acme::MUDLike::footer() }
sub command {
my $self = shift;
my $request = shift;
# this is called by login() immediately after verifying credientials
if($request->request->url->path =~ m/pushstream/) {
# warn "pushstream path_session handling XXX";
my $w = Coro::Event->var(var => \$got_message, poll => 'w');
while(1) {
$w->next;
# warn "got_message diddled XXX";
# on submitting the form without a JS background post, the poll HTTP connection gets broken
$SIG{PIPE} = 'IGNORE';
$request->print( join "<br>\n", map { s{<}{\<}gs; s{\n}{<br>\n}g; $_ } $self->get_messages );
$request->next;
}
}
if($request->request->url->path =~ m/sendmessage/) {
while(1) {
# warn "sendmessage path_session handling XXX";
my $msg = $request->param('message');
$self->parse_command($msg);
# $request->print("Got message.\n");
$request->print($self->get_html_messages());
$request->next;
}
}
#
# players get three execution contexts:
# * one for AJAX message posts without header/footer in the reply
# * one for COMET message pulls
# * the main HTML one below (which might only run once); arbitrarily selected as being the main one cuz its longest
#
$floor->insert($self);
while(1) {
$request->print(header);
#
# chat/commands
#
if($request->param('action') and $request->param('action') eq 'chat') {
# chat messages first so they appear in the log below
# there's only one action defined right now -- chat. everything else hangs off of that.
my $msg = $request->param('message');
$self->parse_command($msg);
};
do {
$request->print(qq{
<b>Chat/Command:</b>
<form method="post" id="f" action="/">
<input type="hidden" name="action" value="chat">
<input type="hidden" id="nick" name="nick" value="@{[ $self->name ]}">
<input type="hidden" id="admin" name="admin" value="$password">
<input type="text" id="message" name="message" size="50">
<!-- <input type="submit" name="sendbutton" value="Send" id="sendbutton"> -->
<input type="submit" name="sendbutton" value="Send" id="sendbutton">
<span id="status"></span>
</form>
<br>
<div id="log">@{[ $self->get_html_messages ]}</div>
});
};
} continue {
$request->print(footer);
$request->next();
} # end while
}
sub parse_command {
my $self = shift;
my $msg = shift;
warn "parse_command: msg: ``$msg''";
$self->tell_object("> $msg");
if($msg and $msg =~ m{^/}) {
my @args = split / /, $msg;
(my $cmd) = shift(@args) =~ m{/(\w+)};
# XXX I'd like to see template matching, like V N A N, then preact/act/postact
if( $self->can("_$cmd") ) {
eval { $self->can("_$cmd")->($self, @args); 1; } or $self->tell_object("Error in command: ``$@''.");
} else {
$self->tell_object("No such command: $cmd.");
}
} elsif($msg) {
$floor->tell_object($self->name . ': ' . $msg); # XXX should be $self->environment->tell_object
# $request->print("Got it!\n");
}
}
sub item_by_arg {
my $self = shift;
my $item = shift;
my $ob;
return $self->current_item if $item eq 'current';
if($item =~ m/^\d+$/) {
my @stuff = $self->environment->contents;
$ob = $stuff[$item] if $item < @stuff;
}
$ob or $ob = $self->inventory->named($item); # thing in our inventory with that name
$ob or $ob = $self->environment->named($item); # thing in our environment with that name
$ob or $ob = $item if exists &{$item.'::new'}; # raw package name
$ob or do {
# Foo::Bar=HASH(0x812ea54)
my $hex;
($hex) = $item =~ m{^[a-z][a-z_:]+\((0x[0-9a-z]+)\)}i;
$hex or ($hex) = $item =~ m{^0x([0-9a-z]+)}i;
if($hex) {
$ob = Devel::Pointer::deref(hex($hex));
}
};
return $ob;
}
lib/Acme/MUDLike.pm view on Meta::CPAN
The environment and players in it all have C<tell_object> methods that takes a string to add to their
message buffer.
Calling C<tell_object> in the environment sends the message to all players.
Objects define various other methods.
=item C<< /who >>
List of who is logged in. Currently the same C</look>.
=item C<< /inventory >>
Or C</i> or C</inv>. Lists the items you are carrying.
=item C<< /clone >>
Creates an instance of an object given a package name. Eg:
/clone sword
=item C<< /take >>
Pick up an item from the floor (the room) and place it in your inventory.
Or alternatively C<< /take item from player >> to take something from someone.
=item C<< /drop >>
Drop an item on the floor.
=item C<< /give >>
Eg:
/give sword to scrottie
Transfers an object to another player.
=item C<< /dest >>
Destroys an object instance.
=back
=head2 new()
Each running program may only have one L<Acme::MUDLike> instance running.
It would be dumb to have two coexisting parallel universes tucked away inside the same program.
Hell, if anything, it would be nice to do some peer discovery, RPC, object serialization, etc,
and share objects between multiple running programs.
=item C<continuity>
Optional. Pass in an existing L<Continuity> instance.
Must have been created with the parameter C<< path_session => 1 >>.
=item C<port>
Optional. Defaults to C<2000>.
This and other parameters, such as those documented in L<Continuity>, are passed through
to C<< Continuity->new() >>.
=item C<password>
Optional. Password to use.
Everyone gets the same password, and anyone with the password can log in with any name.
Otherwise one is pseudo-randomly generated and printed to C<stdout>.
=cut
=head1 HISTORY
=over 8
=item 0.01
Original version; created by h2xs 1.23 with options
-A -C -X -b 5.8.0 -c -n Acme::MUDLike
=back
=head1 TODO
(Major items... additional in the source.)
=item Test. Very, very green right now.
=item Telnet in as well as HTTP.
=item JavaScript vi/L<Acme::SubstituteSubs> integration.
=item Multiple rooms. Right now, there's just one.
The JavaScript based vi and file browser I've been using with L<Acme::SubstituteSubs> isn't in any of my modules
yet so development from within isn't really practical using just these modules.
There's some glue missing.
=head1 SEE ALSO
=item L<Continuity>
=item L<Continuity::Monitor>
=item L<Acme::State>
=item L<Acme::SubstituteSubs>
L<Acme::State> preserves state across runs and L<Acme::SubstituteSubs>.
These three modules work on their own but are complimentary to each other.
Using L<Acme::SubstituteSubs>, the program can be modified in-place without being restarted,
so you don't have to log back in again after each change batch of changes to the code.
Code changes take effect immediately.
L<Acme::State> persists variable values when the program is finally stopped and restarted.
L<Acme::State> will also optionally serialize code references to disc, so you can
C<eval> subs into existance and let it save them to disc for you and then later
use L<B::Deparse> to retrieve a version of the source.
The C<Todo> comments near the top of the source.
=head1 AUTHOR
Scott Walters, E<lt>scott@slowass.netE<gt>
=head1 COPYRIGHT AND LICENSE
lib/Acme/MUDLike.pm view on Meta::CPAN
return this.each( n, arguments );
};
});
jQuery.each( [ "eq", "lt", "gt", "contains" ], function(i,n){
jQuery.fn[ n ] = function(num,fn) {
return this.filter( ":" + n + "(" + num + ")", fn );
};
});
jQuery.each( [ "height", "width" ], function(i,n){
jQuery.fn[ n ] = function(h) {
return h == undefined ?
( this.length ? jQuery.css( this[0], n ) : null ) :
this.css( n, h.constructor == String ? h : h + "px" );
};
});
jQuery.extend({
expr: {
"": "m[2]=='*'||jQuery.nodeName(a,m[2])",
"#": "a.getAttribute('id')==m[2]",
":": {
// Position Checks
lt: "i<m[3]-0",
gt: "i>m[3]-0",
nth: "m[3]-0==i",
eq: "m[3]-0==i",
first: "i==0",
last: "i==r.length-1",
even: "i%2==0",
odd: "i%2",
// Child Checks
"nth-child": "jQuery.nth(a.parentNode.firstChild,m[3],'nextSibling',a)==a",
"first-child": "jQuery.nth(a.parentNode.firstChild,1,'nextSibling')==a",
"last-child": "jQuery.nth(a.parentNode.lastChild,1,'previousSibling')==a",
"only-child": "jQuery.sibling(a.parentNode.firstChild).length==1",
// Parent Checks
parent: "a.firstChild",
empty: "!a.firstChild",
// Text Check
contains: "jQuery.fn.text.apply([a]).indexOf(m[3])>=0",
// Visibility
visible: 'a.type!="hidden"&&jQuery.css(a,"display")!="none"&&jQuery.css(a,"visibility")!="hidden"',
hidden: 'a.type=="hidden"||jQuery.css(a,"display")=="none"||jQuery.css(a,"visibility")=="hidden"',
// Form attributes
enabled: "!a.disabled",
disabled: "a.disabled",
checked: "a.checked",
selected: "a.selected||jQuery.attr(a,'selected')",
// Form elements
text: "a.type=='text'",
radio: "a.type=='radio'",
checkbox: "a.type=='checkbox'",
file: "a.type=='file'",
password: "a.type=='password'",
submit: "a.type=='submit'",
image: "a.type=='image'",
reset: "a.type=='reset'",
button: 'a.type=="button"||jQuery.nodeName(a,"button")',
input: "/input|select|textarea|button/i.test(a.nodeName)"
},
".": "jQuery.className.has(a,m[2])",
"@": {
"=": "z==m[4]",
"!=": "z!=m[4]",
"^=": "z&&!z.indexOf(m[4])",
"$=": "z&&z.substr(z.length - m[4].length,m[4].length)==m[4]",
"*=": "z&&z.indexOf(m[4])>=0",
"": "z",
_resort: function(m){
return ["", m[1], m[3], m[2], m[5]];
},
_prefix: "z=a[m[3]];if(!z||/href|src/.test(m[3]))z=jQuery.attr(a,m[3]);"
},
"[": "jQuery.find(m[2],a).length"
},
// The regular expressions that power the parsing engine
parse: [
// Match: [@value='test'], [@foo]
/^\[ *(@)([a-z0-9_-]*) *([!*$^=]*) *('?"?)(.*?)\4 *\]/i,
// Match: [div], [div p]
/^(\[)\s*(.*?(\[.*?\])?[^[]*?)\s*\]/,
// Match: :contains('foo')
/^(:)([a-z0-9_-]*)\("?'?(.*?(\(.*?\))?[^(]*?)"?'?\)/i,
// Match: :even, :last-chlid
/^([:.#]*)([a-z0-9_*-]*)/i
],
token: [
/^(\/?\.\.)/, "a.parentNode",
/^(>|\/)/, "jQuery.sibling(a.firstChild)",
/^(\+)/, "jQuery.nth(a,2,'nextSibling')",
/^(~)/, function(a){
var s = jQuery.sibling(a.parentNode.firstChild);
return s.slice(jQuery.inArray(a,s) + 1);
}
],
multiFilter: function( expr, elems, not ) {
var old, cur = [];
while ( expr && expr != old ) {
old = expr;
var f = jQuery.filter( expr, elems, not );
expr = f.t.replace(/^\s*,\s*/, "" );
cur = not ? elems = f.r : jQuery.merge( cur, f.r );
}
return cur;
},
find: function( t, context ) {
( run in 0.952 second using v1.01-cache-2.11-cpan-bbe5e583499 )