Imported from archive.
authorJeremy Stanley <fungi@yuggoth.org>
Thu, 22 Sep 2005 23:34:49 +0000 (23:34 +0000)
committerJeremy Stanley <fungi@yuggoth.org>
Thu, 22 Sep 2005 23:34:49 +0000 (23:34 +0000)
* command, menu, mudpy.conf, template, mudpy.py (DataFile.__init__)
(DataFile.save): Changed the control meta-element to __control__ so
as to avoid namespace collisions later.

* command (menu:active), mudpy.py (User.send): Removed the prompt
facet to enable more dynamic prompt generation.

* command (command:move), mudpy.py (Element.go_home, Element.go_to)
(Element.move_direction, command_move): Implemented a move command
and associated backend functions/methods allowing avatars to move
between interconnected locations.

* command (command:show), mudpy.py (command_show): Added a result
parameter to the admin show command, allowing administrative users
to eval arbitrary Python statements.

* menu (menu:activate_avatar), mudpy.py
(User.activate_avatar_by_index): New convenience function to
simplify this menu's action.

* mudpy.conf (internal:limits), mudpy.py (User.authenticate): Added
a default_admins list facet, for use in identifying user names which
should automatically be granted administrative privileges--dangerous
and therefore commented out of the config by default.

* mudpy.py (DataFile.save): When writing new files, any necessary
parent directories in the specified path will be created
automatically. Since the ConfigParser module doesn't make an effort
to sort its contents, replacement code has been added to do this.
(Element.ancestry, Element.append, Element.has_facet)
(Element.remove_facet): Implemented sieve-style recursive facet
inheritence for elements using an inherit meta-facet.
(User.send): Refactored how newlines are added/removed/replaced in
the output queue, to avoid chaining an ugly number of them. Added a
flush flag, allowing urgent output to be pushed through immediately.

command
katarsis
menu
mudpy.conf
mudpy.py
template [new file with mode: 0644]

diff --git a/command b/command
index ded5b45..0c4d8b6 100644 (file)
--- a/command
+++ b/command
@@ -1,4 +1,4 @@
-[control]
+[__control__]
 read_only = yes
 
 [command:create]
@@ -30,8 +30,13 @@ action = command_help(user, parameters)
 description = List commands or get help on one.
 help = This will list all comand words available to you along with a brief description or, alternatively, give you detailed information on one command.
 
+[command:move]
+action = command_move(user, parameters)
+description = Move in a specific direction.
+help = You move in a direction by entering:$(eol)   move north
+
 [command:quit]
-action = user.state = "main_utility"
+action = command_quit(user)
 description = Leave Katarsis.
 help = This will save your account and disconnect your client connection.
 
@@ -56,5 +61,5 @@ help = Invoke it like this:$(eol)$(eol)   set actor:dominique description You se
 action = command_show(user, parameters)
 administrative = yes
 description = Show element data.
-help = Here are the possible incantations:$(eol)$(eol)   show categories$(eol)   show category actor$(eol)   show element location:1:2:3:4$(eol)   show files$(eol)   show time
+help = Here are the possible incantations:$(eol)$(eol)   show categories$(eol)   show category actor$(eol)   show element location:1:2:3:4$(eol)   show files$(eol)   show result user.avatar.get("name")$(eol)   show time
 
index 4cb19de..8e2bb43 100644 (file)
--- a/katarsis
+++ b/katarsis
-[location:127]
-keywords_e = ['booth']
-description = You find the hustle and bustle of this place remarkably distracting. The streets are filled with people of all races, buying and selling goods of every variety. Lust Street runs North and South here while a merchant's booth lies to the East.
-closeable_e = yes
-pickproof_e = yes
-terrain = city
-link_e = location:143
-link_n = location:128
-description_e = This is a simple merchant's booth.
-link_s = location:126
-name = South Lust Street
-
-[location:120]
-description = You find the hustle and bustle of this place remarkably distracting. The streets are filled with people of all races, buying and selling goods of every variety. To the West lies Avarice Avenue while Gluttony Street leads North.
-terrain = city
-name = The Corner of Gluttony and Avarice
-link_w = location:119
-link_n = location:121
-
-[location:143]
-link_w = location:127
+[location:-1,-1,0]
+description = This booth sells cloth garments of every description. Fine silks and linens line the walls, draped from every protrusion and stacked on every surface.
+gridlinks = ['north']
+name = Tailor's Booth
 terrain = inside
-name = New BuildWalk Room
-description = This unfinished room was created by Imp.
 
-[location:132]
-keywords_e = ['booth']
-description = You find the hustle and bustle of this place remarkably distracting. The streets are filled with people of all races, buying and selling goods of every variety. Lust Street runs North and South here while a merchant's booth lies to the East.
-closeable_e = yes
-pickproof_e = yes
+[location:-1,-3,0]
+closeable_south = True
+description = You find the hustle and bustle of this place remarkably distracting. The streets are filled with people of all races, buying and selling goods of every variety. Avarice Avenue runs East and West here while a storefront beckons to the South.
+description_south = This is the entrance to a shop.
+gridlinks = ['east', 'west']
+keywords_south = ['shop']
+link_south = location:146
+name = West Avarice Avenue
+pickproof_south = True
 terrain = city
-link_e = location:141
-link_n = location:133
-description_e = This is a simple merchant's booth.
-link_s = location:131
-name = North Lust Street
 
-[location:159]
-link_s = location:101
-terrain = inside
-name = Tanner's Booth
-description = The smell of leather goods fills your nostrils. Tanned, finished and embroidered leather is hung and piled everywhere.
-
-[location:158]
-name = Cobbler's Booth
-terrain = inside
-closeable_s = yes
-keywords_s = ['booth']
-link_s = location:129
-pickproof_s = yes
-description = Shoes, shoes and more shoes... You need footwear, we have footwear. Look around, see what you like. Best prices in town! So you gonna buy something, stranger?
-
-[location:122]
-keywords_e = ['shop']
-description = You find the hustle and bustle of this place remarkably distracting. The streets are filled with people of all races, buying and selling goods of every variety. Gluttony Street runs North and South here while a storefront beckons to the East.
-closeable_e = yes
-pickproof_e = yes
+[location:-1,0,0]
+closeable_south = True
+description = You find the hustle and bustle of this place remarkably distracting. The streets are filled with people of all races, buying and selling goods of every variety. Envy Avenue runs East and West here while a merchant's booth lies to the South.
+description_south = This is a simple merchant's booth.
+gridlinks = ['east', 'south', 'west']
+keywords_south = ['booth']
+name = West Envy Avenue
+pickproof_south = True
 terrain = city
-link_e = location:150
-link_n = location:103
-description_e = This is the entrance to a shop.
-link_s = location:121
-name = South Gluttony Street
 
-[location:151]
-terrain = inside
-name = New BuildWalk Room
-link_e = location:105
-description = This unfinished room was created by Imp.
-
-[location:150]
-link_w = location:122
-terrain = inside
-name = New BuildWalk Room
-description = This unfinished room was created by Imp.
-
-[location:153]
-terrain = inside
-link_n = location:108
-name = New BuildWalk Room
-description = This unfinished room was created by Imp.
-
-[location:152]
-link_w = location:106
-terrain = inside
-name = New BuildWalk Room
-description = This unfinished room was created by Imp.
-
-[location:155]
-link_w = location:114
-terrain = inside
-name = New BuildWalk Room
-description = This unfinished room was created by Imp.
+[location:-1,3,0]
+closeable_south = True
+description = You find the hustle and bustle of this place remarkably distracting. The streets are filled with people of all races, buying and selling goods of every variety. Sloth Avenue runs East and West here while a merchant's booth lies to the South.
+description_south = This is a simple merchant's booth.
+gridlinks = ['east', 'west']
+keywords_south = ['booth']
+link_south = location:139
+name = West Sloth Avenue
+pickproof_south = True
+terrain = city
 
-[location:154]
-link_s = location:109
-terrain = inside
-name = New BuildWalk Room
-description = This unfinished room was created by Imp.
+[location:-2,-3,0]
+closeable_north = True
+description = You find the hustle and bustle of this place remarkably distracting. The streets are filled with people of all races, buying and selling goods of every variety. Avarice Avenue runs East and West here while a merchant's booth lies to the North.
+description_north = This is a simple merchant's booth.
+gridlinks = ['east', 'west']
+keywords_north = ['booth']
+link_north = location:145
+name = West Avarice Avenue
+pickproof_north = True
+terrain = city
 
-[location:157]
-terrain = inside
-link_n = location:130
-name = Tailor's Booth
-description = This booth sells cloth garments of every description. Fine silks and linens line the walls, draped from every protrusion and stacked on every surface.
+[location:-2,0,0]
+closeable_north = True
+description = You find the hustle and bustle of this place remarkably distracting. The streets are filled with people of all races, buying and selling goods of every variety. Envy Avenue runs East and West here while a merchant's booth lies to the North.
+description_north = This is a simple merchant's booth.
+gridlinks = ['east', 'north', 'west']
+keywords_north = ['booth']
+name = West Envy Avenue
+pickproof_north = True
+terrain = city
 
-[location:156]
+[location:-2,1,0]
+closeable_south = True
+description = Shoes, shoes and more shoes... You need footwear, we have footwear. Look around, see what you like. Best prices in town! So you gonna buy something, stranger?
+gridlinks = ['south']
+keywords_south = ['booth']
+name = Cobbler's Booth
+pickproof_south = True
 terrain = inside
-name = New BuildWalk Room
-link_e = location:115
-description = This unfinished room was created by Imp.
 
-[location:115]
-link_n = location:114
-description = You find the hustle and bustle of this place remarkably distracting. The streets are filled with people of all races, buying and selling goods of every variety. Anger Street runs North and South here while a merchant's booth lies to the West.
+[location:-2,3,0]
+closeable_north = True
+description = You find the hustle and bustle of this place remarkably distracting. The streets are filled with people of all races, buying and selling goods of every variety. Sloth Avenue runs East and West here while a storefront beckons to the North.
+description_north = This is the entrance to a shop.
+gridlinks = ['east', 'west']
+keywords_north = ['shop']
+link_north = location:140
+name = West Sloth Avenue
+pickproof_north = True
 terrain = city
-keywords_w = ['booth']
-closeable_w = yes
-description_w = This is a simple merchant's booth.
-link_s = location:116
-pickproof_w = yes
-link_w = location:156
-name = South Anger Street
 
-[location:114]
-keywords_e = ['booth']
-description = You find the hustle and bustle of this place remarkably distracting. The streets are filled with people of all races, buying and selling goods of every variety. Anger Street runs North and South here while a merchant's booth lies to the East.
-closeable_e = yes
-pickproof_e = yes
+[location:-3,-1,0]
+closeable_east = True
+description = You find the hustle and bustle of this place remarkably distracting. The streets are filled with people of all races, buying and selling goods of every variety. Lust Street runs North and South here while a merchant's booth lies to the East.
+description_east = This is a simple merchant's booth.
+gridlinks = ['north', 'south']
+keywords_east = ['booth']
+link_east = location:143
+name = South Lust Street
+pickproof_east = True
 terrain = city
-link_e = location:155
-link_n = location:161
-description_e = This is a simple merchant's booth.
-link_s = location:115
-name = South Anger Street
 
-[location:117]
-description_s = This gate leads from the market to the city proper.
-link_n = location:116
-description = You find the hustle and bustle of this place remarkably distracting. The streets are filled with people of all races, buying and selling goods of every variety. To the North, Anger Street heads into the market while the rest of the city lies trough the gate to the South.
+[location:-3,-2,0]
+closeable_west = True
+description = You find the hustle and bustle of this place remarkably distracting. The streets are filled with people of all races, buying and selling goods of every variety. Lust Street runs North and South here while a storefront beckons to the West.
+description_west = This is the entrance to a shop.
+gridlinks = ['north', 'south']
+keywords_west = ['shop']
+link_west = location:144
+name = South Lust Street
+pickproof_west = True
 terrain = city
-closeable_s = yes
-keywords_s = ['gate']
-link_s = location:0
-pickproof_s = yes
-name = South Market Gate
 
-[location:116]
-link_n = location:115
-description = You find the hustle and bustle of this place remarkably distracting. The streets are filled with people of all races, buying and selling goods of every variety. To the East and West lies Avarice Avenue while Anger Street runs North and South.
+[location:-3,-3,0]
+description = You find the hustle and bustle of this place remarkably distracting. The streets are filled with people of all races, buying and selling goods of every variety. To the East lies Avarice Avenue while Lust Street leads North.
+gridlinks = ['east', 'north']
+name = The Corner of Lust and Avarice
 terrain = city
-link_e = location:118
-link_s = location:117
-link_w = location:123
-name = South Anger and Avarice
 
-[location:111]
-pickproof_n = yes
-name = North Market Gate
-closeable_n = yes
+[location:-3,0,0]
+description = You find the hustle and bustle of this place remarkably distracting. The streets are filled with people of all races, buying and selling goods of every variety. To the East and West lies Envy Avenue while Lust Street runs North and South.
+gridlinks = ['east', 'north', 'south', 'west']
+name = Lust and West Envy
 terrain = city
-link_n = location:0
-keywords_n = ['gate']
-link_s = location:110
-description_n = This gate leads from the market to the city proper.
-description = You find the hustle and bustle of this place remarkably distracting. The streets are filled with people of all races, buying and selling goods of every variety. To the South, Anger Street heads into the market while the rest of the city lies trough the gate to the North.
 
-[location:110]
-link_n = location:111
-description = You find the hustle and bustle of this place remarkably distracting. The streets are filled with people of all races, buying and selling goods of every variety. To the East and West lies Sloth Avenue while Anger Street runs North and South.
+[location:-3,1,0]
+closeable_west = True
+description = You find the hustle and bustle of this place remarkably distracting. The streets are filled with people of all races, buying and selling goods of every variety. Lust Street runs North and South here while a storefront beckons to the West.
+description_west = This is the entrance to a shop.
+gridlinks = ['north', 'south']
+keywords_west = ['shop']
+link_west = location:142
+name = North Lust Street
+pickproof_west = True
 terrain = city
-link_e = location:109
-link_s = location:112
-link_w = location:135
-name = North Anger and Sloth
 
-[location:113]
-link_n = location:112
-description = You find the hustle and bustle of this place remarkably distracting. The streets are filled with people of all races, buying and selling goods of every variety. Anger Street runs North and South here while a merchant's booth lies to the West.
+[location:-3,2,0]
+closeable_east = True
+description = You find the hustle and bustle of this place remarkably distracting. The streets are filled with people of all races, buying and selling goods of every variety. Lust Street runs North and South here while a merchant's booth lies to the East.
+description_east = This is a simple merchant's booth.
+gridlinks = ['north', 'south']
+keywords_east = ['booth']
+link_east = location:141
+name = North Lust Street
+pickproof_east = True
 terrain = city
-keywords_w = ['booth']
-closeable_w = yes
-description_w = This is a simple merchant's booth.
-link_s = location:161
-pickproof_w = yes
-link_w = location:137
-name = North Anger Street
 
-[location:112]
-keywords_e = ['booth']
-description = You find the hustle and bustle of this place remarkably distracting. The streets are filled with people of all races, buying and selling goods of every variety. Anger Street runs North and South here while a merchant's booth lies to the East.
-closeable_e = yes
-pickproof_e = yes
+[location:-3,3,0]
+description = You find the hustle and bustle of this place remarkably distracting. The streets are filled with people of all races, buying and selling goods of every variety. To the East lies Sloth Avenue while Lust Street leads South.
+gridlinks = ['east', 'south']
+name = The Corner of Lust and Sloth
 terrain = city
-link_e = location:138
-link_n = location:110
-description_e = This is a simple merchant's booth.
-link_s = location:113
-name = North Anger Street
 
-[location:137]
-terrain = inside
-name = New BuildWalk Room
-link_e = location:113
-description = This unfinished room was created by Imp.
-
-[location:136]
-description_w = This gate leads from the market to the city proper.
+[location:-4,0,0]
+closeable_west = True
 description = You find the hustle and bustle of this place remarkably distracting. The streets are filled with people of all races, buying and selling goods of every variety. To the East, Envy Avenue heads into the market while the rest of the city lies trough the gate to the West.
-terrain = city
-link_e = location:128
-keywords_w = ['gate']
-closeable_w = yes
-pickproof_w = yes
-link_w = location:0
+description_west = This gate leads from the market to the city proper.
+gridlinks = ['east']
+keywords_west = ['gate']
+link_west = location:0
 name = West Market Gate
-
-[location:135]
-description_s = This is a simple merchant's booth.
-name = West Sloth Avenue
-terrain = city
-link_e = location:110
-closeable_s = yes
-keywords_s = ['booth']
-link_s = location:139
-link_w = location:134
-pickproof_s = yes
-description = You find the hustle and bustle of this place remarkably distracting. The streets are filled with people of all races, buying and selling goods of every variety. Sloth Avenue runs East and West here while a merchant's booth lies to the South.
-
-[location:134]
-pickproof_n = yes
-name = West Sloth Avenue
-closeable_n = yes
+pickproof_west = True
 terrain = city
-link_e = location:135
-link_n = location:140
-keywords_n = ['shop']
-description_n = This is the entrance to a shop.
-link_w = location:133
-description = You find the hustle and bustle of this place remarkably distracting. The streets are filled with people of all races, buying and selling goods of every variety. Sloth Avenue runs East and West here while a storefront beckons to the North.
 
-[location:119]
-description_s = This is the entrance to a shop.
-name = East Avarice Avenue
+[location:0,-1,0]
+closeable_east = True
+description = You find the hustle and bustle of this place remarkably distracting. The streets are filled with people of all races, buying and selling goods of every variety. Anger Street runs North and South here while a merchant's booth lies to the East.
+description_east = This is a simple merchant's booth.
+gridlinks = ['north', 'south']
+keywords_east = ['booth']
+link_east = location:155
+name = South Anger Street
+pickproof_east = True
 terrain = city
-link_e = location:120
-closeable_s = yes
-keywords_s = ['shop']
-link_s = location:148
-link_w = location:118
-pickproof_s = yes
-description = You find the hustle and bustle of this place remarkably distracting. The streets are filled with people of all races, buying and selling goods of every variety. Avarice Avenue runs East and West here while a storefront beckons to the South.
 
-[location:118]
-pickproof_n = yes
-name = East Avarice Avenue
-closeable_n = yes
+[location:0,-2,0]
+closeable_west = True
+description = You find the hustle and bustle of this place remarkably distracting. The streets are filled with people of all races, buying and selling goods of every variety. Anger Street runs North and South here while a merchant's booth lies to the West.
+description_west = This is a simple merchant's booth.
+gridlinks = ['north', 'south']
+keywords_west = ['booth']
+link_west = location:156
+name = South Anger Street
+pickproof_west = True
 terrain = city
-link_e = location:119
-link_n = location:147
-keywords_n = ['booth']
-description_n = This is a simple merchant's booth.
-link_w = location:116
-description = You find the hustle and bustle of this place remarkably distracting. The streets are filled with people of all races, buying and selling goods of every variety. Avarice Avenue runs East and West here while a merchant's booth lies to the North.
 
-[location:131]
-link_n = location:132
-description = You find the hustle and bustle of this place remarkably distracting. The streets are filled with people of all races, buying and selling goods of every variety. Lust Street runs North and South here while a storefront beckons to the West.
+[location:0,-3,0]
+description = You find the hustle and bustle of this place remarkably distracting. The streets are filled with people of all races, buying and selling goods of every variety. To the East and West lies Avarice Avenue while Anger Street runs North and South.
+gridlinks = ['east', 'north', 'south', 'west']
+name = South Anger and Avarice
 terrain = city
-keywords_w = ['shop']
-closeable_w = yes
-description_w = This is the entrance to a shop.
-link_s = location:128
-pickproof_w = yes
-link_w = location:142
-name = North Lust Street
 
-[location:130]
-description_s = This is a simple merchant's booth.
-name = West Envy Avenue
+[location:0,-4,0]
+closeable_south = True
+description = You find the hustle and bustle of this place remarkably distracting. The streets are filled with people of all races, buying and selling goods of every variety. To the North, Anger Street heads into the market while the rest of the city lies trough the gate to the South.
+description_south = This gate leads from the market to the city proper.
+gridlinks = ['north']
+keywords_south = ['gate']
+link_south = location:0
+name = South Market Gate
+pickproof_south = True
 terrain = city
-link_e = location:161
-closeable_s = yes
-keywords_s = ['booth']
-link_s = location:157
-link_w = location:129
-pickproof_s = yes
-description = You find the hustle and bustle of this place remarkably distracting. The streets are filled with people of all races, buying and selling goods of every variety. Envy Avenue runs East and West here while a merchant's booth lies to the South.
 
-[location:123]
-description_s = This is the entrance to a shop.
-name = West Avarice Avenue
+[location:0,0,0]
+description = You find the hustle and bustle of this place remarkably distracting. The streets are filled with people of all races, buying and selling goods of every variety. To the East and West lies Envy Avenue while Anger Street runs North and South. A short flight of stairs leads up to the bandstand.
+gridlinks = ['east', 'north', 'south', 'up', 'west']
+name = Pride Square
 terrain = city
-link_e = location:116
-closeable_s = yes
-keywords_s = ['shop']
-link_s = location:146
-link_w = location:124
-pickproof_s = yes
-description = You find the hustle and bustle of this place remarkably distracting. The streets are filled with people of all races, buying and selling goods of every variety. Avarice Avenue runs East and West here while a storefront beckons to the South.
-
-[location:148]
-terrain = inside
-link_n = location:119
-name = New BuildWalk Room
-description = This unfinished room was created by Imp.
-
-[location:149]
-terrain = inside
-name = New BuildWalk Room
-link_e = location:121
-description = This unfinished room was created by Imp.
-
-[location:146]
-terrain = inside
-link_n = location:123
-name = New BuildWalk Room
-description = This unfinished room was created by Imp.
-
-[location:147]
-link_s = location:118
-terrain = inside
-name = New BuildWalk Room
-description = This unfinished room was created by Imp.
-
-[location:144]
-terrain = inside
-name = New BuildWalk Room
-link_e = location:126
-description = This unfinished room was created by Imp.
-
-[location:145]
-link_s = location:124
-terrain = inside
-name = New BuildWalk Room
-description = This unfinished room was created by Imp.
-
-[location:142]
-terrain = inside
-name = New BuildWalk Room
-link_e = location:131
-description = This unfinished room was created by Imp.
-
-[location:138]
-link_w = location:112
-terrain = inside
-name = New BuildWalk Room
-description = This unfinished room was created by Imp.
-
-[location:140]
-link_s = location:134
-terrain = inside
-name = New BuildWalk Room
-description = This unfinished room was created by Imp.
 
-[location:141]
-link_w = location:132
+[location:0,0,1]
+description = The stand is currently not in use for a performance, but shoppers gather here to rest and chat with one another. A pleasantly cool breeze coupled with a nice view of the market make this a good place to relax.
+gridlinks = ['down']
+name = The Bandstand
 terrain = inside
-name = New BuildWalk Room
-description = This unfinished room was created by Imp.
 
-[location:160]
-terrain = inside
-link_n = location:102
-name = Blacksmith's Booth
-description = You find yourself amidst cold, hard steel and iron equipment of every possible description. A slight breeze brings tinkle and clank sounds from all around you.
-
-[location:161]
-link_n = location:113
-description = You find the hustle and bustle of this place remarkably distracting. The streets are filled with people of all races, buying and selling goods of every variety. To the East and West lies Envy Avenue while Anger Street runs North and South. A short flight of stairs leads up to the bandstand.
+[location:0,1,0]
+closeable_west = True
+description = You find the hustle and bustle of this place remarkably distracting. The streets are filled with people of all races, buying and selling goods of every variety. Anger Street runs North and South here while a merchant's booth lies to the West.
+description_west = This is a simple merchant's booth.
+gridlinks = ['north', 'south']
+keywords_west = ['booth']
+link_west = location:137
+name = North Anger Street
+pickproof_west = True
 terrain = city
-link_e = location:101
-link_s = location:114
-link_w = location:130
-link_u = location:100
-name = Pride Square
 
-[location:108]
-description_s = This is a simple merchant's booth.
-name = East Sloth Avenue
+[location:0,2,0]
+closeable_east = True
+description = You find the hustle and bustle of this place remarkably distracting. The streets are filled with people of all races, buying and selling goods of every variety. Anger Street runs North and South here while a merchant's booth lies to the East.
+description_east = This is a simple merchant's booth.
+gridlinks = ['north', 'south']
+keywords_east = ['booth']
+link_east = location:138
+name = North Anger Street
+pickproof_east = True
 terrain = city
-link_e = location:107
-closeable_s = yes
-keywords_s = ['booth']
-link_s = location:153
-link_w = location:109
-pickproof_s = yes
-description = You find the hustle and bustle of this place remarkably distracting. The streets are filled with people of all races, buying and selling goods of every variety. Sloth Avenue runs East and West here while a merchant's booth lies to the South.
 
-[location:109]
-pickproof_n = yes
-name = East Sloth Avenue
-closeable_n = yes
+[location:0,3,0]
+description = You find the hustle and bustle of this place remarkably distracting. The streets are filled with people of all races, buying and selling goods of every variety. To the East and West lies Sloth Avenue while Anger Street runs North and South.
+gridlinks = ['east', 'north', 'south', 'west']
+name = North Anger and Sloth
 terrain = city
-link_e = location:108
-link_n = location:154
-keywords_n = ['shop']
-description_n = This is the entrance to a shop.
-link_w = location:110
-description = You find the hustle and bustle of this place remarkably distracting. The streets are filled with people of all races, buying and selling goods of every variety. Sloth Avenue runs East and West here while a storefront beckons to the North.
 
-[location:128]
-link_n = location:131
-description = You find the hustle and bustle of this place remarkably distracting. The streets are filled with people of all races, buying and selling goods of every variety. To the East and West lies Envy Avenue while Lust Street runs North and South.
+[location:0,4,0]
+closeable_north = True
+description = You find the hustle and bustle of this place remarkably distracting. The streets are filled with people of all races, buying and selling goods of every variety. To the South, Anger Street heads into the market while the rest of the city lies trough the gate to the North.
+description_north = This gate leads from the market to the city proper.
+gridlinks = ['south']
+keywords_north = ['gate']
+link_north = location:0
+name = North Market Gate
+pickproof_north = True
 terrain = city
-link_e = location:129
-link_s = location:127
-link_w = location:136
-name = Lust and West Envy
 
-[location:139]
-terrain = inside
-link_n = location:135
-name = New BuildWalk Room
-description = This unfinished room was created by Imp.
+[location:1,-3,0]
+closeable_north = True
+description = You find the hustle and bustle of this place remarkably distracting. The streets are filled with people of all races, buying and selling goods of every variety. Avarice Avenue runs East and West here while a merchant's booth lies to the North.
+description_north = This is a simple merchant's booth.
+gridlinks = ['east', 'west']
+keywords_north = ['booth']
+link_north = location:147
+name = East Avarice Avenue
+pickproof_north = True
+terrain = city
 
-[location:102]
-description_s = This is a simple merchant's booth.
+[location:1,0,0]
+closeable_north = True
+description = You find the hustle and bustle of this place remarkably distracting. The streets are filled with people of all races, buying and selling goods of every variety. Envy Avenue runs East and West here while a merchant's booth lies to the North.
+description_north = This is a simple merchant's booth.
+gridlinks = ['east', 'north', 'west']
+keywords_north = ['booth']
 name = East Envy Avenue
+pickproof_north = True
 terrain = city
-link_e = location:103
-closeable_s = yes
-keywords_s = ['booth']
-link_s = location:160
-link_w = location:101
-pickproof_s = yes
-description = You find the hustle and bustle of this place remarkably distracting. The streets are filled with people of all races, buying and selling goods of every variety. Envy Avenue runs East and West here while a merchant's booth lies to the South.
 
-[location:103]
-link_n = location:105
-description = You find the hustle and bustle of this place remarkably distracting. The streets are filled with people of all races, buying and selling goods of every variety. To the East and West lies Envy Avenue while Gluttony Street runs North and South.
+[location:1,1,0]
+description = The smell of leather goods fills your nostrils. Tanned, finished and embroidered leather is hung and piled everywhere.
+gridlinks = ['south']
+name = Tanner's Booth
+terrain = inside
+
+[location:1,3,0]
+closeable_north = True
+description = You find the hustle and bustle of this place remarkably distracting. The streets are filled with people of all races, buying and selling goods of every variety. Sloth Avenue runs East and West here while a storefront beckons to the North.
+description_north = This is the entrance to a shop.
+gridlinks = ['east', 'west']
+keywords_north = ['shop']
+link_north = location:154
+name = East Sloth Avenue
+pickproof_north = True
 terrain = city
-link_e = location:104
-link_s = location:122
-link_w = location:102
-name = Gluttony and East Envy
 
-[location:100]
-link_d = location:161
+[location:2,-1,0]
+description = You find yourself amidst cold, hard steel and iron equipment of every possible description. A slight breeze brings tinkle and clank sounds from all around you.
+gridlinks = ['north']
+name = Blacksmith's Booth
 terrain = inside
-description = The stand is currently not in use for a performance, but shoppers gather here to rest and chat with one another. A pleasantly cool breeze coupled with a nice view of the market make this a good place to relax and recover energy.
-name = The Bandstand
 
-[location:101]
-pickproof_n = yes
-name = East Envy Avenue
-closeable_n = yes
+[location:2,-3,0]
+closeable_south = True
+description = You find the hustle and bustle of this place remarkably distracting. The streets are filled with people of all races, buying and selling goods of every variety. Avarice Avenue runs East and West here while a storefront beckons to the South.
+description_south = This is the entrance to a shop.
+gridlinks = ['east', 'west']
+keywords_south = ['shop']
+link_south = location:148
+name = East Avarice Avenue
+pickproof_south = True
 terrain = city
-link_e = location:102
-link_n = location:159
-keywords_n = ['booth']
-description_n = This is a simple merchant's booth.
-link_w = location:161
-description = You find the hustle and bustle of this place remarkably distracting. The streets are filled with people of all races, buying and selling goods of every variety. Envy Avenue runs East and West here while a merchant's booth lies to the North.
 
-[location:106]
-keywords_e = ['shop']
-description = You find the hustle and bustle of this place remarkably distracting. The streets are filled with people of all races, buying and selling goods of every variety. Gluttony Street runs North and South here while a storefront beckons to the East.
-closeable_e = yes
-pickproof_e = yes
+[location:2,0,0]
+closeable_south = True
+description = You find the hustle and bustle of this place remarkably distracting. The streets are filled with people of all races, buying and selling goods of every variety. Envy Avenue runs East and West here while a merchant's booth lies to the South.
+description_south = This is a simple merchant's booth.
+gridlinks = ['east', 'south', 'west']
+keywords_south = ['booth']
+name = East Envy Avenue
+pickproof_south = True
 terrain = city
-link_e = location:152
-link_n = location:107
-description_e = This is the entrance to a shop.
-link_s = location:105
-name = North Gluttony Street
 
-[location:121]
-link_n = location:122
-description = You find the hustle and bustle of this place remarkably distracting. The streets are filled with people of all races, buying and selling goods of every variety. Gluttony Street runs North and South here while a merchant's booth lies to the West.
+[location:2,3,0]
+closeable_south = True
+description = You find the hustle and bustle of this place remarkably distracting. The streets are filled with people of all races, buying and selling goods of every variety. Sloth Avenue runs East and West here while a merchant's booth lies to the South.
+description_south = This is a simple merchant's booth.
+gridlinks = ['east', 'west']
+keywords_south = ['booth']
+link_south = location:153
+name = East Sloth Avenue
+pickproof_south = True
 terrain = city
-keywords_w = ['booth']
-closeable_w = yes
-description_w = This is a simple merchant's booth.
-link_s = location:120
-pickproof_w = yes
-link_w = location:149
-name = South Gluttony Street
 
-[location:104]
-keywords_e = ['gate']
-name = East Market Gate
-closeable_e = yes
-pickproof_e = yes
+[location:3,-1,0]
+closeable_east = True
+description = You find the hustle and bustle of this place remarkably distracting. The streets are filled with people of all races, buying and selling goods of every variety. Gluttony Street runs North and South here while a storefront beckons to the East.
+description_east = This is the entrance to a shop.
+gridlinks = ['north', 'south']
+keywords_east = ['shop']
+link_east = location:150
+name = South Gluttony Street
+pickproof_east = True
 terrain = city
-link_e = location:0
-description_e = This gate leads from the market to the city proper.
-link_w = location:103
-description = You find the hustle and bustle of this place remarkably distracting. The streets are filled with people of all races, buying and selling goods of every variety. To the West, Envy Avenue heads into the market while the rest of the city lies trough the gate to the East.
 
-[location:105]
-link_n = location:106
+[location:3,-2,0]
+closeable_west = True
 description = You find the hustle and bustle of this place remarkably distracting. The streets are filled with people of all races, buying and selling goods of every variety. Gluttony Street runs North and South here while a merchant's booth lies to the West.
+description_west = This is a simple merchant's booth.
+gridlinks = ['north', 'south']
+keywords_west = ['booth']
+link_west = location:149
+name = South Gluttony Street
+pickproof_west = True
 terrain = city
-keywords_w = ['booth']
-closeable_w = yes
-description_w = This is a simple merchant's booth.
-link_s = location:103
-pickproof_w = yes
-link_w = location:151
-name = North Gluttony Street
 
-[location:124]
-pickproof_n = yes
-name = West Avarice Avenue
-closeable_n = yes
+[location:3,-3,0]
+description = You find the hustle and bustle of this place remarkably distracting. The streets are filled with people of all races, buying and selling goods of every variety. To the West lies Avarice Avenue while Gluttony Street leads North.
+gridlinks = ['north', 'west']
+name = The Corner of Gluttony and Avarice
 terrain = city
-link_e = location:123
-link_n = location:145
-keywords_n = ['booth']
-description_n = This is a simple merchant's booth.
-link_w = location:125
-description = You find the hustle and bustle of this place remarkably distracting. The streets are filled with people of all races, buying and selling goods of every variety. Avarice Avenue runs East and West here while a merchant's booth lies to the North.
 
-[location:133]
-name = The Corner of Lust and Sloth
+[location:3,0,0]
+description = You find the hustle and bustle of this place remarkably distracting. The streets are filled with people of all races, buying and selling goods of every variety. To the East and West lies Envy Avenue while Gluttony Street runs North and South.
+gridlinks = ['east', 'north', 'south', 'west']
+name = Gluttony and East Envy
 terrain = city
-link_e = location:134
-link_s = location:132
-description = You find the hustle and bustle of this place remarkably distracting. The streets are filled with people of all races, buying and selling goods of every variety. To the East lies Sloth Avenue while Lust Street leads South.
 
-[location:129]
-pickproof_n = yes
-name = West Envy Avenue
-closeable_n = yes
+[location:3,1,0]
+closeable_west = True
+description = You find the hustle and bustle of this place remarkably distracting. The streets are filled with people of all races, buying and selling goods of every variety. Gluttony Street runs North and South here while a merchant's booth lies to the West.
+description_west = This is a simple merchant's booth.
+gridlinks = ['north', 'south']
+keywords_west = ['booth']
+link_west = location:151
+name = North Gluttony Street
+pickproof_west = True
 terrain = city
-link_e = location:130
-link_n = location:158
-keywords_n = ['booth']
-description_n = This is a simple merchant's booth.
-link_w = location:128
-description = You find the hustle and bustle of this place remarkably distracting. The streets are filled with people of all races, buying and selling goods of every variety. Envy Avenue runs East and West here while a merchant's booth lies to the North.
 
-[location:125]
-link_n = location:126
+[location:3,2,0]
+closeable_east = True
+description = You find the hustle and bustle of this place remarkably distracting. The streets are filled with people of all races, buying and selling goods of every variety. Gluttony Street runs North and South here while a storefront beckons to the East.
+description_east = This is the entrance to a shop.
+gridlinks = ['north', 'south']
+keywords_east = ['shop']
+link_east = location:152
+name = North Gluttony Street
+pickproof_east = True
 terrain = city
-link_e = location:124
-name = The Corner of Lust and Avarice
-description = You find the hustle and bustle of this place remarkably distracting. The streets are filled with people of all races, buying and selling goods of every variety. To the East lies Avarice Avenue while Lust Street leads North.
 
-[location:107]
+[location:3,3,0]
+description = You find the hustle and bustle of this place remarkably distracting. The streets are filled with people of all races, buying and selling goods of every variety. To the West lies Sloth Avenue while Gluttony Street leads South.
+gridlinks = ['south', 'west']
 name = The Corner of Gluttony and Sloth
 terrain = city
-link_s = location:106
-link_w = location:108
-description = You find the hustle and bustle of this place remarkably distracting. The streets are filled with people of all races, buying and selling goods of every variety. To the West lies Sloth Avenue while Gluttony Street leads South.
 
-[location:126]
-link_n = location:127
-description = You find the hustle and bustle of this place remarkably distracting. The streets are filled with people of all races, buying and selling goods of every variety. Lust Street runs North and South here while a storefront beckons to the West.
+[location:4,0,0]
+closeable_east = True
+description = You find the hustle and bustle of this place remarkably distracting. The streets are filled with people of all races, buying and selling goods of every variety. To the West, Envy Avenue heads into the market while the rest of the city lies trough the gate to the East.
+description_east = This gate leads from the market to the city proper.
+gridlinks = ['west']
+keywords_east = ['gate']
+link_east = location:0
+name = East Market Gate
+pickproof_east = True
 terrain = city
-keywords_w = ['shop']
-closeable_w = yes
-description_w = This is the entrance to a shop.
-link_s = location:125
-pickproof_w = yes
-link_w = location:144
-name = South Lust Street
 
diff --git a/menu b/menu
index eb1d270..460c1b8 100644 (file)
--- a/menu
+++ b/menu
@@ -1,10 +1,9 @@
-[control]
+[__control__]
 read_only = yes
 
 [menu:activate_avatar]
-action = user.avatar = universe.contents[user.account.getlist("avatars")[int(choice)-1]]
+action = user.activate_avatar_by_index(int(choice)-1)
 action_a = pass
-branch = active
 branch_a = main_utility
 choice_a = abort selection
 create = dict([(str(x+1),y) for x,y in enumerate(user.list_avatar_names())])
@@ -13,7 +12,6 @@ description = This is the list of avatars available for you to awaken.
 prompt = Whom would you like to awaken?
 
 [menu:active]
-prompt = >
 
 [menu:checking_new_account_name]
 action_d = user.account.destroy()
index ebc81e6..8b921eb 100644 (file)
@@ -1,6 +1,6 @@
-[control]
+[__control__]
 default_files = {"account": "account", "actor": "actor", "command": "command", "internal": "internal", "location": "location", "menu": "menu", "other": "other" }
-include_files = katarsis
+include_files = ["katarsis", "template"]
 private_files = account
 read_only = yes
 
@@ -14,6 +14,7 @@ punctuation_muse = ...
 punctuation_say = .
 
 [internal:limits]
+#default_admins = admin
 max_avatars = 7
 password_tries = 3
 
index af61429..7d87bf9 100644 (file)
--- a/mudpy.py
+++ b/mudpy.py
@@ -9,6 +9,7 @@ from md5 import new as new_md5
 from os import R_OK, access, chmod, makedirs, stat
 from os.path import abspath, dirname, exists, isabs, join as path_join
 from random import choice, randrange
+from re import match
 from socket import AF_INET, SO_REUSEADDR, SOCK_STREAM, SOL_SOCKET, socket
 from stat import S_IMODE, ST_MODE
 from syslog import LOG_PID, LOG_INFO, LOG_DAEMON, closelog, openlog, syslog
@@ -19,6 +20,8 @@ class Element:
        """An element of the universe."""
        def __init__(self, key, universe, origin=""):
                """Default values for the in-memory element variables."""
+               self.owner = None
+               self.contents = {}
                self.key = key
                if self.key.find(":") > 0:
                        self.category, self.subkey = self.key.split(":", 1)
@@ -48,31 +51,63 @@ class Element:
                if universe.files[self.origin].data.has_option(self.key, facet):
                        universe.files[self.origin].data.remove_option(self.key, facet)
        def facets(self):
-               """Return a list of facets for this element."""
+               """Return a list of non-inherited facets for this element."""
                return universe.files[self.origin].data.options(self.key)
+       def has_facet(self, facet):
+               """Return whether the non-inherited facet exists."""
+               return facet in self.facets()
+       def remove_facet(self, facet):
+               """Remove a facet from the element."""
+               if self.has_facet(facet): universe.files[self.origin].data.remove_option(self.key, facet)
+       def ancestry(self):
+               """Return a list of the element's inheritance lineage."""
+               if self.has_facet("inherit"):
+                       ancestry = self.getlist("inherit")
+                       for parent in ancestry[:]:
+                               ancestors = universe.contents[parent].ancestry()
+                               for ancestor in ancestors:
+                                       if ancestor not in ancestry: ancestry.append(ancestor)
+                       return ancestry
+               else: return []
        def get(self, facet, default=None):
                """Retrieve values."""
                if default is None: default = ""
                if universe.files[self.origin].data.has_option(self.key, facet):
                        return universe.files[self.origin].data.get(self.key, facet)
+               elif self.has_facet("inherit"):
+                       for ancestor in self.ancestry():
+                               if universe.contents[ancestor].has_facet(facet):
+                                       return universe.contents[ancestor].get(facet)
                else: return default
        def getboolean(self, facet, default=None):
                """Retrieve values as boolean type."""
                if default is None: default=False
                if universe.files[self.origin].data.has_option(self.key, facet):
                        return universe.files[self.origin].data.getboolean(self.key, facet)
+               elif self.has_facet("inherit"):
+                       for ancestor in self.ancestry():
+                               if universe.contents[ancestor].has_facet(facet):
+                                       return universe.contents[ancestor].getboolean(facet)
                else: return default
        def getint(self, facet, default=None):
                """Return values as int/long type."""
                if default is None: default = 0
                if universe.files[self.origin].data.has_option(self.key, facet):
                        return universe.files[self.origin].data.getint(self.key, facet)
+               elif self.has_facet("inherit"):
+                       for ancestor in self.ancestry():
+                               if universe.contents[ancestor].has_facet(facet):
+                                       return universe.contents[ancestor].getint(facet)
                else: return default
        def getfloat(self, facet, default=None):
                """Return values as float type."""
                if default is None: default = 0.0
                if universe.files[self.origin].data.has_option(self.key, facet):
                        return universe.files[self.origin].data.getfloat(self.key, facet)
+               elif self.has_facet("inherit"):
+                       for ancestor in self.ancestry():
+                               if universe.contents[ancestor].has_facet(facet):
+                                       return universe.contents[ancestor].getfloat(facet)
                else: return default
        def getlist(self, facet, default=None):
                """Return values as list type."""
@@ -91,6 +126,79 @@ class Element:
                if type(value) is long: value = str(value)
                elif not type(value) is str: value = repr(value)
                universe.files[self.origin].data.set(self.key, facet, value)
+       def append(self, facet, value):
+               """Append value tp a list."""
+               if type(value) is long: value = str(value)
+               elif not type(value) is str: value = repr(value)
+               newlist = self.getlist(facet)
+               newlist.append(value)
+               self.set(facet, newlist)
+       def send(self, message, eol="$(eol)"):
+               """Convenience method to pass messages to an owner."""
+               if self.owner: self.owner.send(message, eol)
+       def go_to(self, location):
+               """Relocate the element to a specific location."""
+               current = self.get("location")
+               if current and current in universe.contents[current].contents:
+                       del universe.contents[current].contents[self.key]
+               if location in universe.contents: self.set("location", location)
+               universe.contents[location].contents[self.key] = self
+               self.look_at(location)
+       def go_home(self):
+               """Relocate the element to its default location."""
+               self.go_to(self.get("default_location"))
+       def move_direction(self, direction):
+               """Relocate the element in a specified direction."""
+               self.go_to(universe.contents[self.get("location")].link_neighbor(direction))
+       def look_at(self, key):
+               """Show an element to another element."""
+               if self.owner:
+                       element = universe.contents[key]
+                       message = ""
+                       name = element.get("name")
+                       if name: message += "$(cyn)" + name + "$(nrm)$(eol)"
+                       description = element.get("description")
+                       if description: message += description + "$(eol)"
+                       portal_list = element.portals().keys()
+                       if portal_list:
+                               portal_list.sort()
+                               message += "$(cyn)[ Exits: " + ", ".join(portal_list) + " ]$(nrm)$(eol)"
+                       for element in universe.contents[self.get("location")].contents.values():
+                               if element.getboolean("is_actor") and element is not self:
+                                       message += "$(yel)" + element.get("name") + " is here.$(nrm)$(eol)"
+                       self.send(message)
+       def portals(self):
+               """Map the portal directions for a room to neighbors."""
+               portals = {}
+               if match("""^location:-?\d+,-?\d+,-?\d+$""", self.key):
+                       coordinates = [(int(x)) for x in self.key.split(":")[1].split(",")]
+                       offsets = {
+                               "down": (0,0,-1),
+                               "east": (1,0,0),
+                               "north": (0,1,0),
+                               "south": (0,-1,0),
+                               "up": (0,0,1),
+                               "west": (-1,0,0)
+                               }
+                       for portal in self.getlist("gridlinks"):
+                               adjacent = map(lambda c,o: c+o, coordinates, offsets[portal])
+                               neighbor = "location:" + ",".join([(str(x)) for x in adjacent])
+                               if neighbor in universe.contents: portals[portal] = neighbor
+               for facet in self.facets():
+                       if facet.startswith("link_"):
+                               neighbor = self.get(facet)
+                               if neighbor in universe.contents:
+                                       portal = facet.split("_")[1]
+                                       portals[portal] = neighbor
+               return portals
+       def link_neighbor(self, direction):
+               """Return the element linked in a given direction."""
+               portals = self.portals()
+               if direction in portals: return portals[direction]
+       def echo_to_location(self, message):
+               """Show a message to other elements in the current location."""
+               for element in universe.contents[self.get("location")].contents.values():
+                       if element is not self: element.send(message)
 
 class DataFile:
        """A file containing universe elements."""
@@ -99,37 +207,59 @@ class DataFile:
                if access(filename, R_OK): self.data.read(filename)
                self.filename = filename
                universe.files[filename] = self
-               if self.data.has_option("control", "include_files"):
-                       includes = makelist(self.data.get("control", "include_files"))
+               if self.data.has_option("__control__", "include_files"):
+                       includes = makelist(self.data.get("__control__", "include_files"))
                else: includes = []
-               if self.data.has_option("control", "default_files"):
-                       origins = makedict(self.data.get("control", "default_files"))
+               if self.data.has_option("__control__", "default_files"):
+                       origins = makedict(self.data.get("__control__", "default_files"))
                        for key in origins.keys():
                                if not key in includes: includes.append(key)
                                universe.default_origins[key] = origins[key]
                                if not key in universe.categories:
                                        universe.categories[key] = {}
-               if self.data.has_option("control", "private_files"):
-                       for item in makelist(self.data.get("control", "private_files")):
+               if self.data.has_option("__control__", "private_files"):
+                       for item in makelist(self.data.get("__control__", "private_files")):
                                if not item in includes: includes.append(item)
                                if not item in universe.private_files:
                                        if not isabs(item):
                                                item = path_join(dirname(filename), item)
                                        universe.private_files.append(item)
                for section in self.data.sections():
-                       if section != "control":
+                       if section != "__control__":
                                Element(section, universe, filename)
                for include_file in includes:
                        if not isabs(include_file):
                                include_file = path_join(dirname(filename), include_file)
                        DataFile(include_file, universe)
        def save(self):
-               if ( self.data.sections() or exists(self.filename) ) and not ( self.data.has_option("control", "read_only") and self.data.getboolean("control", "read_only") ):
-                       if not exists(dirname(self.filename)): makedirs(dirname(self.filename))
+               """Write the data, if necessary."""
+
+               # when there is content or the file exists, but is not read-only
+               if ( self.data.sections() or exists(self.filename) ) and not ( self.data.has_option("__control__", "read_only") and self.data.getboolean("__control__", "read_only") ):
+
+                       # make parent directories if necessary
+                       if not exists(dirname(self.filename)):
+                               makedirs(dirname(self.filename))
+
+                       # our data file
                        file_descriptor = file(self.filename, "w")
+
+                       # if it's marked private, chmod it appropriately
                        if self.filename in universe.private_files and oct(S_IMODE(stat(self.filename)[ST_MODE])) != 0600:
                                chmod(self.filename, 0600)
-                       self.data.write(file_descriptor)
+
+                       # write it back sorted, instead of using ConfigParser
+                       sections = self.data.sections()
+                       sections.sort()
+                       for section in sections:
+                               file_descriptor.write("[" + section + "]\n")
+                               options = self.data.options(section)
+                               options.sort()
+                               for option in options:
+                                       file_descriptor.write(option + " = " + self.data.get(section, option) + "\n")
+                               file_descriptor.write("\n")
+
+                       # flush and close the file
                        file_descriptor.flush()
                        file_descriptor.close()
 
@@ -220,6 +350,7 @@ class User:
                else: message = "An unnamed user"
                message += " logged out."
                log(message)
+               self.deactivate_avatar()
                self.connection.close()
                self.remove()
 
@@ -246,6 +377,8 @@ class User:
                        "output_queue",
                        "partial_input",
                        "echoing",
+                       "terminator",
+                       "negotiation_pause",
                        "avatar",
                        "account"
                        ]:
@@ -271,13 +404,13 @@ class User:
 
                                # make a note of it
                                log("User " + self.account.get("name") + " reconnected--closing old connection to " + old_user.address + ".")
-                               old_user.send("$(eol)$(red)New connection from " + self.address + ". Terminating old connection...$(nrm)$(eol)")
-                               self.send("$(eol)$(red)Taking over old connection from " + old_user.address + ".$(nrm)")
+                               old_user.send("$(eol)$(red)New connection from " + self.address + ". Terminating old connection...$(nrm)$(eol)", flush=True, add_prompt=False)
 
                                # close the old connection
                                old_user.connection.close()
 
                                # replace the old connection with this one
+                               old_user.send("$(eol)$(red)Taking over old connection from " + old_user.address + ".$(nrm)")
                                old_user.connection = self.connection
                                old_user.last_address = old_user.address
                                old_user.address = self.address
@@ -297,6 +430,8 @@ class User:
                if not self.state is "authenticated":
                        log("User " + self.account.get("name") + " logged in.")
                        self.authenticated = True
+                       if self.account.subkey in universe.categories["internal"]["limits"].getlist("default_admins"):
+                               self.account.set("administrator", "True")
 
        def show_menu(self):
                """Send the user their current menu."""
@@ -316,12 +451,16 @@ class User:
                """Remove a user from the list of connected users."""
                universe.userlist.remove(self)
 
-       def send(self, output, eol="$(eol)", raw=False):
+       def send(self, output, eol="$(eol)", raw=False, flush=False, add_prompt=True):
                """Send arbitrary text to a connected user."""
 
                # unless raw mode is on, clean it up all nice and pretty
                if not raw:
 
+                       # strip extra $(eol) off if present
+                       while output.startswith("$(eol)"): output = output[6:]
+                       while output.endswith("$(eol)"): output = output[:-6]
+
                        # we'll take out GA or EOR and add them back on the end
                        if output.endswith(IAC+GA) or output.endswith(IAC+EOR):
                                terminate = True
@@ -331,7 +470,10 @@ class User:
                        # start with a newline, append the message, then end
                        # with the optional eol string passed to this function
                        # and the ansi escape to return to normal text
-                       output = "\r\n" + output + eol + chr(27) + "[0m"
+                       output = "$(eol)" + output + eol + chr(27) + "[0m"
+
+                       # tack on a prompt if active
+                       if self.state == "active" and add_prompt: output += "$(eol)> "
 
                        # find and replace macros in the output
                        output = replace_macros(self, output)
@@ -345,6 +487,9 @@ class User:
                # drop the output into the user's output queue
                self.output_queue.append(output)
 
+               # if this is urgent, flush all pending output
+               if flush: self.flush()
+
        def pulse(self):
                """All the things to do to the user per increment."""
 
@@ -359,29 +504,29 @@ class User:
                        else: self.state = "entering_account_name"
 
                # show the user a menu as needed
-               else: self.show_menu()
+               elif not self.state == "active": self.show_menu()
+
+               # flush any pending output in teh queue
+               self.flush()
 
                # disconnect users with the appropriate state
                if self.state == "disconnecting": self.quit()
 
-               # the user is unique and not flagged to disconnect
-               else:
-               
-                       # try to send the last item in the queue and remove it
-                       if self.output_queue:
-                               try:
-                                       self.connection.send(self.output_queue[0])
-                                       del self.output_queue[0]
+               # check for input and add it to the queue
+               self.enqueue_input()
 
-                               # but if we can't, that's okay too
-                               except:
-                                       pass
+               # there is input waiting in the queue
+               if self.input_queue: handle_user_input(self)
 
-                       # check for input and add it to the queue
-                       self.enqueue_input()
+       def flush(self):
+               """Try to send the last item in the queue and remove it."""
+               if self.output_queue:
+                       try:
+                               self.connection.send(self.output_queue[0])
+                               del self.output_queue[0]
+                       except:
+                               pass
 
-                       # there is input waiting in the queue
-                       if self.input_queue: handle_user_input(self)
 
        def enqueue_input(self):
                """Process and enqueue any new input."""
@@ -529,9 +674,8 @@ class User:
                counter = 0
                while "avatar:" + self.account.get("name") + ":" + str(counter) in universe.categories["actor"].keys(): counter += 1
                self.avatar = Element("actor:avatar:" + self.account.get("name") + ":" + str(counter), universe)
-               avatars = self.account.getlist("avatars")
-               avatars.append(self.avatar.key)
-               self.account.set("avatars", avatars)
+               self.avatar.append("inherit", "template:actor")
+               self.account.append("avatars", self.avatar.key)
 
        def delete_avatar(self, avatar):
                """Remove an avatar from the world and from the user's list."""
@@ -541,6 +685,23 @@ class User:
                avatars.remove(avatar)
                self.account.set("avatars", avatars)
 
+       def activate_avatar_by_index(self, index):
+               """Enter the world with a particular indexed avatar."""
+               self.avatar = universe.contents[self.account.getlist("avatars")[index]]
+               self.avatar.owner = self
+               self.state = "active"
+               self.avatar.go_home()
+
+       def deactivate_avatar(self):
+               """Have the active avatar leave the world."""
+               if self.avatar:
+                       current = self.avatar.get("location")
+                       self.avatar.set("default_location", current)
+                       del universe.contents[current].contents[self.avatar.key]
+                       self.avatar.remove_facet("location")
+                       self.avatar.owner = None
+                       self.avatar = None
+
        def destroy(self):
                """Destroy the user and associated avatars."""
                for avatar in self.account.getlist("avatars"): self.delete_avatar(avatar)
@@ -706,8 +867,12 @@ def replace_macros(user, text, is_input=False):
                        "$(bld)": chr(27) + "[1m",
                        "$(nrm)": chr(27) + "[0m",
                        "$(blk)": chr(27) + "[30m",
+                       "$(blu)": chr(27) + "[34m",
+                       "$(cyn)": chr(27) + "[36m",
                        "$(grn)": chr(27) + "[32m",
+                       "$(mgt)": chr(27) + "[35m",
                        "$(red)": chr(27) + "[31m",
+                       "$(yel)": chr(27) + "[33m",
                        }
 
                # add dynamic macros where possible
@@ -1167,6 +1332,11 @@ def command_reload(user):
        # set a flag to reload
        universe.reload_modules = True
 
+def command_quit(user):
+       """Leave the world and go back to the main menu."""
+       user.deactivate_avatar()
+       user.state = "main_utility"
+
 def command_help(user, parameters):
        """List available commands and provide help for commands."""
 
@@ -1220,6 +1390,12 @@ def command_help(user, parameters):
        # send the accumulated output to the user
        user.send(output)
 
+def command_move(user, parameters):
+       """Move the avatar in a given direction."""
+       if parameters in universe.contents[user.avatar.get("location")].portals():
+               user.avatar.move_direction(parameters)
+       else: user.send("You cannot go that way.")
+
 def command_say(user, parameters):
        """Speak to others in the same room."""
 
@@ -1260,8 +1436,8 @@ def command_say(user, parameters):
                        message = message.replace(" " + word + " ", " " + word.capitalize() + " ")
 
                # tell the room
-               # TODO: we won't be using broadcast once there are actual rooms
-               broadcast(user.avatar.get("name") + " " + action + "s, \"" + message + "\"")
+               user.avatar.echo_to_location(user.avatar.get("name") + " " + action + "s, \"" + message + "\"")
+               user.send("You " + action + ", \"" + message + "\"")
 
        # there was no message
        else:
@@ -1301,6 +1477,12 @@ def command_show(user, parameters):
                                facets.sort()
                                for facet in facets:
                                        message += "$(eol)   $(grn)" + facet + ": $(red)" + escape_macros(element.get(facet)) + "$(nrm)"
+               elif arguments[0] == "result":
+                       if len(arguments) > 1:
+                               try:
+                                       message = repr(eval(" ".join(arguments[1:])))
+                               except:
+                                       message = "Your expression raised an exception!"
        if not message:
                if parameters: message = "I don't know what \"" + parameters + "\" is."
                else: message = "What do you want to show?"
diff --git a/template b/template
new file mode 100644 (file)
index 0000000..1dd5d10
--- /dev/null
+++ b/template
@@ -0,0 +1,7 @@
+[__control__]
+read_only = yes
+
+[template:actor]
+default_location = location:0,0,1
+is_actor = yes
+