Michael Granger avatar Michael Granger committed 33f0e3f

Checkpoint commit.
* Changed README.markdown to README.md and blockquoted the Verse license
statement.
* Updated build system.
* Fixed dependency issues in local rakefile that caused node source generation to
be an all-or-nothing affair.
* Made a new verse server wrapper that will eventually be the Ruby correllary of
the reference verse server.
* Modified the "dead-simple" server to reflect API changes.
* Removed typedefs in favor of using plain structs.
* Updated node-type boilerplate.
* Started work on Verse::ObjectNode.
* Started initial implementations of #create_node and #delete_node
to Verse::Session.
* Moved the verse callback hooks to the initializer rather than #add_observer,
as callbacks are global.
* Started implementation of Verse::Server.
* Removed unused Verse::Session.synchronize.

Comments (0)

Files changed (24)

 \.o$
 ^tmp/
 hostkey.rsa
+^\.yardoc
+docs/api
+ChangeLog
+\.DS_Store

README.markdown

-# Ruby-Verse
-
-This is a Ruby binding for the Verse network protocol. Verse is a network 
-protocol that lets multiple applications act together as one large application by 
-sharing data over a network. 
-
-You can check out the current development source with Mercurial like so:
-
-    hg clone http://bitbucket.org/ged/ruby-verse
-
-You can submit bug reports, suggestions, and read more about future plans at the
-[project page](http://bitbucket.org/ged/ruby-verse).
-
-## Basic Design
-
-The API was inspired by [Ample](http://www.elmindreda.org/verse/ample/), a C++ wrapper for Verse by
-Camilla Berglund <elmindreda@elmindreda.org>.
-
-Ruby-Verse consists of several core module functions for interacting with a Verse server, a
-collection of Node classes, and mixin modules for adding Verse-awareness to your classes.
-
-### Namespace
-
-All classes and modules in Ruby-Verse are declared inside the _Verse_ module.
-
-### Classes
-
-* Session (Observable)
-* Node (Observable)
-	- ObjectNode
-		* ObjectLink
-		* ObjectTransform
-		* ObjectMethod
-		* ObjectMethodGroup
-		* ObjectAnimation
-	- GeometryNode
-		* GeometryBone
-		* GeometryLayer
-		* GeometryCrease
-	- MaterialNode
-		* MaterialFragment
-	- TextNode
-		* TextBuffer
-	- BitmapNode
-		* BitmapLayer
-	- CurveNode
-	  * CurveKey
-	- AudioNode
-	  * AudioBuffer
-	  * AudioStream
-
-
-### Observer Mixins
-
-* PingObserver
-* ConnectionObserver
-* SessionObserver
-
-* NodeObserver
-	- NodeTagObserver
-	- NodeTagGroupObserver
-* ObjectNodeObserver
-	- ObjectLinkObserver
-	- ObjectTransformObserver
-	- ObjectMethodObserver
-	- ObjectMethodGroupObserver
-* GeometryNodeObserver
-	- GeometryBoneObserver
-	- GeometryLayerObserver
-	- GeometryCreaseObserver
-* MaterialNodeObserver
-	- FragmentObserver
-* TextNodeObserver
-	- TextBufferObserver
-* BitmapNodeObserver
-	- BitmapLayerObserver
-* CurveNodeObserver
-* AudioNodeObserver
-  - AudioBufferObserver
-  - AudioStreamObserver
-
-## License
-
-Portions of this source code were derived from the Verse source, and are used under the 
-following licensing terms:
-
-  Copyright (c) 2005-2008, The Uni-Verse Consortium.
-  All rights reserved.
-  
-  Redistribution and use in source and binary forms, with or without
-  modification, are permitted provided that the following conditions are
-  met:
-  
-   * Redistributions of source code must retain the above copyright
-     notice, this list of conditions and the following disclaimer.
-   * Redistributions in binary form must reproduce the above copyright
-     notice, this list of conditions and the following disclaimer in the
-     documentation and/or other materials provided with the distribution.
-  
-  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
-  IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
-  TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
-  PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER
-  OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
-  EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
-  PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
-  PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
-  LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
-  NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
-  SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-  
-
-See the included LICENSE file for licensing details for this library.
-
+# Ruby-Verse
+
+This is a Ruby binding for Verse. Verse is a network protocol that lets multiple applications act
+together as one large application by sharing data over a network.
+
+You can check out the current development source with Mercurial like so:
+
+    hg clone http://bitbucket.org/ged/ruby-verse
+
+You can submit bug reports, suggestions, and read more about future plans at the
+[project page](http://bitbucket.org/ged/ruby-verse).
+
+## Basic Design
+
+The API was inspired by [Ample](http://www.elmindreda.org/verse/ample/), a C++ wrapper for Verse by
+Camilla Berglund <elmindreda@elmindreda.org>.
+
+Ruby-Verse consists of several core module functions for interacting with a Verse server, a
+collection of Node classes, and mixin modules for adding Verse-awareness to your classes.
+
+### Namespace
+
+All classes and modules in Ruby-Verse are declared inside the _Verse_ module.
+
+### Classes
+
+* Session (Observable)
+* Node (Observable)
+	- ObjectNode
+		* ObjectLink
+		* ObjectTransform
+		* ObjectMethod
+		* ObjectMethodGroup
+		* ObjectAnimation
+	- GeometryNode
+		* GeometryBone
+		* GeometryLayer
+		* GeometryCrease
+	- MaterialNode
+		* MaterialFragment
+	- TextNode
+		* TextBuffer
+	- BitmapNode
+		* BitmapLayer
+	- CurveNode
+	  * CurveKey
+	- AudioNode
+	  * AudioBuffer
+	  * AudioStream
+
+
+### Observer Mixins
+
+* PingObserver
+* ConnectionObserver
+* SessionObserver
+
+* NodeObserver
+	- NodeTagObserver
+	- NodeTagGroupObserver
+* ObjectNodeObserver
+	- ObjectLinkObserver
+	- ObjectTransformObserver
+	- ObjectMethodObserver
+	- ObjectMethodGroupObserver
+* GeometryNodeObserver
+	- GeometryBoneObserver
+	- GeometryLayerObserver
+	- GeometryCreaseObserver
+* MaterialNodeObserver
+	- FragmentObserver
+* TextNodeObserver
+	- TextBufferObserver
+* BitmapNodeObserver
+	- BitmapLayerObserver
+* CurveNodeObserver
+* AudioNodeObserver
+  - AudioBufferObserver
+  - AudioStreamObserver
+
+## License
+
+Portions of this source code were derived from the Verse source, and are used under the 
+following licensing terms:
+
+> Copyright (c) 2005-2008, The Uni-Verse Consortium.
+> All rights reserved.
+> 
+> Redistribution and use in source and binary forms, with or without
+> modification, are permitted provided that the following conditions are
+> met:
+> 
+>  * Redistributions of source code must retain the above copyright
+>    notice, this list of conditions and the following disclaimer.
+>  * Redistributions in binary form must reproduce the above copyright
+>    notice, this list of conditions and the following disclaimer in the
+>    documentation and/or other materials provided with the distribution.
+> 
+> THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
+> IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+> TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
+> PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER
+> OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+> EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+> PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+> PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+> LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+> NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+> SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+  
+See the included [LICENSE](LICENSE.html) file for licensing details for this library.
+
 
 ARTIFACTS_DIR = Pathname.new( CC_BUILD_ARTIFACTS )
 
-TEXT_FILES    = Rake::FileList.new( %w[Rakefile ChangeLog README LICENSE] )
+TEXT_FILES    = Rake::FileList.new( %w[Rakefile ChangeLog README* LICENSE] )
 BIN_FILES     = Rake::FileList.new( "#{BINDIR}/*" )
 LIB_FILES     = Rake::FileList.new( "#{LIBDIR}/**/*.rb" )
 EXT_FILES     = Rake::FileList.new( "#{EXTDIR}/**/*.{c,h,rb}" )
 
 # Documentation constants
 API_DOCSDIR = DOCSDIR + 'api'
+README_FILE = TEXT_FILES.find {|path| path =~ /^README/ } || 'README'
 RDOC_OPTIONS = [
 	'--tab-width=4',
 	'--show-hash',
 	'--include', BASEDIR.to_s,
-	'--main=README',
+	"--main=#{README_FILE}",
 	"--title=#{PKG_NAME}",
   ]
 YARD_OPTIONS = [
 	'--use-cache',
 	'--no-private',
 	'--protected',
-	'-r', 'README',
+	'-r', README_FILE,
 	'--exclude', 'extconf\\.rb',
 	'--files', 'ChangeLog,LICENSE',
 	'--output-dir', API_DOCSDIR.to_s,
 
 	gem.has_rdoc          = true
 	gem.rdoc_options      = RDOC_OPTIONS
-	gem.extra_rdoc_files  = %w[ChangeLog README LICENSE]
+	gem.extra_rdoc_files  = TEXT_FILES - [ 'Rakefile' ]
 
 	gem.bindir            = BINDIR.relative_path_from(BASEDIR).to_s
 	gem.executables       = BIN_FILES.select {|pn| File.executable?(pn) }.
 
 NODE_TYPES = %w[audio bitmap curve geometry material object text]
 NODE_TYPE_REGEX = %r{#{EXTDIR}/(#{Regexp.union(NODE_TYPES)})node\.c}
-NODE_TYPE_SOURCES = NODE_TYPES.collect {|nodetype| EXTDIR + "#{nodetype}node.c" }
+NODE_TYPE_SOURCE_PATHS = NODE_TYPES.
+	collect {|nodetype| EXTDIR + "#{nodetype}node.c" }.
+	collect {|path| path.to_s }
+NODE_TYPE_SOURCES = Rake::FileList[ *NODE_TYPE_SOURCE_PATHS ]
 
 NODE_TYPE_TEMPLATE = EXTDIR + 'nodeclass.template'
 
 	Rake::Task[ :node_classes ].invoke
 end
 
+file *NODE_TYPE_SOURCES
 task :node_classes => NODE_TYPE_SOURCES
 task :compile => NODE_TYPE_SOURCES
 
+#!/usr/bin/env ruby
+
+require 'trollop'
+
+require 'verse'
+require 'verse/server'
+
+# Reference Verse Server frontend.
+# $Id$
+# 
+# This is a port of the reference server that comes with the Verse source. Please see 
+# the README file for details.
+# 
+# @author Michael Granger <ged@FaerieMUD.org>
+# 
+# Copyright (c) 2010 The FaerieMUD Consortium
+# 
+# All rights reserved.
+# 
+# Redistribution and use in source and binary forms, with or without modification, are
+# permitted provided that the following conditions are met:
+# 
+#  * Redistributions of source code must retain the above copyright notice, this
+#    list of conditions and the following disclaimer.
+#  
+#  * Redistributions in binary form must reproduce the above copyright notice, this
+#    list of conditions and the following disclaimer in the documentation and/or
+#    other materials provided with the distribution.
+#  
+#  * Neither the name of the authors, nor the names of its contributors may be used to
+#    endorse or promote products derived from this software without specific prior
+#    written permission.
+# 
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+# 
+# 
+#
+
+opts = Trollop.options do
+	progname = File.basename( $0 )
+	banner "#{progname} [OPTIONS]"
+	opt :port, "The port to listen on", :default => Verse::DEFAULT_PORT
+end
+
+Verse::Server.new( opts ).run
+
+

examples/deadsimple_server.rb

 #!/usr/bin/env ruby1.9
 #coding: utf-8
 
+# Add dead-simple authentication to the base server. 
+
 BEGIN {
 	require 'pathname'
 
 require 'logger'
 require 'pathname'
 
-HOST_ID_FILE = Pathname( "hostkey.rsa" )
-PORT = 4555
-
-Verse.logger.level = Logger::DEBUG
-
-sessions = {}
-my_hostid = nil
-
-Verse.port = PORT
-if HOST_ID_FILE.exist?
-	puts "Using existing hostid..."
-	my_hostid = HOST_ID_FILE.read( encoding: "ascii-8bit" )
-else
-	puts "Creating a new hostid..."
-	my_hostid = Verse.create_host_id
-	HOST_ID_FILE.open( File::WRONLY|File::CREAT|File::EXCL, encoding: "ascii-8bit" ) do |fh|
-		fh.print( my_hostid )
-	end
-end
-Verse.host_id = my_hostid
-
-
-class Server
+class MyServer < Verse::Server
 	require 'digest/sha1'
 
 	include Verse::ConnectionObserver
 
-	USERS = { 'bargle' => 'a76282512cb523ec1f82c79b4f51e34cdd5f11ea' }
-
-	def initialize
-		@connections = {}
-	end
+	USERS = {
+		'bargle' => 'a76282512cb523ec1f82c79b4f51e34cdd5f11ea'
+	}
 
 	def on_connect( name, pass, address, hostid )
-	    Verse.connect_terminate( address, "wrong hostid" ) unless hostid == my_hostid
-		Verse.connect_terminate( address, "auth failed" ) unless self.authenticate( user, pass )
-	    avatar = Verse::ObjectNode.new
-	    session = Verse.connect_accept( avatar, address, my_hostid )
-	    @connections[ address ] = { :avatar => avatar, :session => session }
+		return unless self.authenticate( user, pass )
+		super
 	end
 
 	def authenticate( user, pass )
 		return USERS[ user ] == Digest::SHA1.hexdigest( pass )
 	end
 
-	def on_connect_terminate( address, message )
-		puts "#{address} terminated connection: #{message}"
-		conn = @connections.delete( address )
-		conn[:avatar].destroy
-	end
-
 end # class Server
 
-signal_handler = lambda {|signal| $running = false; Signal.trap(signal, 'IGN') }
-Signal.trap( 'INT', &signal_handler )
+MyServer.new( :port => 5618 ).run
 
-puts "Listening on port #{PORT}"
-$running = true
-Verse::Session.update while $running
-puts "Shutting down."
-
  * Mark the audio part of a node.
  */
 static void
-rbverse_audionode_gc_mark( rbverse_NODE *ptr ) {
+rbverse_audionode_gc_mark( struct rbverse_node *ptr ) {
 	if ( ptr ) {
 		rb_gc_mark( ptr->audio.buffers );
 		rb_gc_mark( ptr->audio.streams );
  * Free the audio part of a node.
  */
 static void
-rbverse_audionode_gc_free( rbverse_NODE *ptr ) {
+rbverse_audionode_gc_free( struct rbverse_node *ptr ) {
 	if ( ptr ) {
 		ptr->audio.buffers = Qnil;
 		ptr->audio.streams = Qnil;
  */
 static VALUE
 rbverse_verse_audionode_initialize( VALUE self ) {
-	rbverse_NODE *ptr;
+	struct rbverse_node *ptr;
 
 	rb_call_super( 0, NULL );
 
 	ptr = rbverse_get_node( self );
+	ptr->type = V_NT_AUDIO;
 
 	ptr->audio.buffers = rb_ary_new();
 	ptr->audio.streams = rb_ary_new();
  * Mark the bitmap part of a node.
  */
 static void
-rbverse_bitmapnode_gc_mark( rbverse_NODE *ptr ) {
+rbverse_bitmapnode_gc_mark( struct rbverse_node *ptr ) {
 	if ( ptr ) {
 		/* TODO: Mark child-specific data */
 	}
  * Free the bitmap part of a node.
  */
 static void
-rbverse_bitmapnode_gc_free( rbverse_NODE *ptr ) {
+rbverse_bitmapnode_gc_free( struct rbverse_node *ptr ) {
 	if ( ptr ) {
 		/* TODO: Free child-specific data */
 	}
  */
 static VALUE
 rbverse_verse_bitmapnode_initialize( VALUE self ) {
-	rbverse_NODE *ptr;
+	struct rbverse_node *ptr;
 
 	rb_call_super( 0, NULL );
 
 	ptr = rbverse_get_node( self );
+	ptr->type = V_NT_BITMAP;
 
 	/* TODO: Initialize bitmap-specific instance data. */
 
  * Mark the curve part of a node.
  */
 static void
-rbverse_curvenode_gc_mark( rbverse_NODE *ptr ) {
+rbverse_curvenode_gc_mark( struct rbverse_node *ptr ) {
 	if ( ptr ) {
 		/* TODO: Mark child-specific data */
 	}
  * Free the curve part of a node.
  */
 static void
-rbverse_curvenode_gc_free( rbverse_NODE *ptr ) {
+rbverse_curvenode_gc_free( struct rbverse_node *ptr ) {
 	if ( ptr ) {
 		/* TODO: Free child-specific data */
 	}
  */
 static VALUE
 rbverse_verse_curvenode_initialize( VALUE self ) {
-	rbverse_NODE *ptr;
+	struct rbverse_node *ptr;
 
 	rb_call_super( 0, NULL );
 
 	ptr = rbverse_get_node( self );
+	ptr->type = V_NT_CURVE;
 
 	/* TODO: Initialize curve-specific instance data. */
 
 	/* Class methods */
 	rbverse_cVerseCurveNode = rb_define_class_under( rbverse_mVerse, "CurveNode", rbverse_cVerseNode );
 
+    /* Constants */
+	rb_define_const( rbverse_cVerseCurveNode, "TYPE_NUMBER", INT2FIX(V_NT_CURVE) );
+
 	/* Initializer */
 	rb_define_method( rbverse_cVerseCurveNode, "initialize", rbverse_verse_curvenode_initialize, 0 );
 
 require 'fileutils'
 
 $CFLAGS << ' -Wall'
-$CFLAGS << ' -ggdb' << ' -DDEBUG' if $DEBUG
+$CFLAGS << ' -ggdb' << ' -DDEBUG'
 
 def fail( *messages )
 	$stderr.puts( *messages )

ext/geometrynode.c

  * Mark the geometry part of a node.
  */
 static void
-rbverse_geometrynode_gc_mark( rbverse_NODE *ptr ) {
+rbverse_geometrynode_gc_mark( struct rbverse_node *ptr ) {
 	if ( ptr ) {
 		/* TODO: Mark child-specific data */
 	}
  * Free the geometry part of a node.
  */
 static void
-rbverse_geometrynode_gc_free( rbverse_NODE *ptr ) {
+rbverse_geometrynode_gc_free( struct rbverse_node *ptr ) {
 	if ( ptr ) {
 		/* TODO: Free child-specific data */
 	}
  */
 static VALUE
 rbverse_verse_geometrynode_initialize( VALUE self ) {
-	rbverse_NODE *ptr;
+	struct rbverse_node *ptr;
 
 	rb_call_super( 0, NULL );
 
 	ptr = rbverse_get_node( self );
+	ptr->type = V_NT_GEOMETRY;
 
 	/* TODO: Initialize geometry-specific instance data. */
 
 	/* Class methods */
 	rbverse_cVerseGeometryNode = rb_define_class_under( rbverse_mVerse, "GeometryNode", rbverse_cVerseNode );
 
+    /* Constants */
+	rb_define_const( rbverse_cVerseGeometryNode, "TYPE_NUMBER", INT2FIX(V_NT_GEOMETRY) );
+
 	/* Initializer */
 	rb_define_method( rbverse_cVerseGeometryNode, "initialize", rbverse_verse_geometrynode_initialize, 0 );
 

ext/materialnode.c

  * Mark the material part of a node.
  */
 static void
-rbverse_materialnode_gc_mark( rbverse_NODE *ptr ) {
+rbverse_materialnode_gc_mark( struct rbverse_node *ptr ) {
 	if ( ptr ) {
 		/* TODO: Mark child-specific data */
 	}
  * Free the material part of a node.
  */
 static void
-rbverse_materialnode_gc_free( rbverse_NODE *ptr ) {
+rbverse_materialnode_gc_free( struct rbverse_node *ptr ) {
 	if ( ptr ) {
 		/* TODO: Free child-specific data */
 	}
  */
 static VALUE
 rbverse_verse_materialnode_initialize( VALUE self ) {
-	rbverse_NODE *ptr;
+	struct rbverse_node *ptr;
 
 	rb_call_super( 0, NULL );
 
 	ptr = rbverse_get_node( self );
+	ptr->type = V_NT_MATERIAL;
 
 	/* TODO: Initialize material-specific instance data. */
 
 	/* Class methods */
 	rbverse_cVerseMaterialNode = rb_define_class_under( rbverse_mVerse, "MaterialNode", rbverse_cVerseNode );
 
+    /* Constants */
+	rb_define_const( rbverse_cVerseMaterialNode, "TYPE_NUMBER", INT2FIX(V_NT_MATERIAL) );
+
 	/* Initializer */
 	rb_define_method( rbverse_cVerseMaterialNode, "initialize", rbverse_verse_materialnode_initialize, 0 );
 
 #include "verse_ext.h"
 
 VALUE rbverse_cVerseNode;
+VALUE rbverse_mVerseNodeObserver;
 
 /* Mapping of V_NT_* enum to the equivalent Verse::Node subclass */
 VALUE rbverse_nodetype_to_nodeclass[ V_NT_NUM_TYPES ];
 
 /* Vtables for mark and free functions for child types. These are
  * populated by the initializers for each of the child types. */
-void ( *node_mark_funcs[V_NT_NUM_TYPES] )(rbverse_NODE *) = {};
-void ( *node_free_funcs[V_NT_NUM_TYPES] )(rbverse_NODE *) = {};
+void ( *node_mark_funcs[V_NT_NUM_TYPES] )(struct rbverse_node *) = {};
+void ( *node_free_funcs[V_NT_NUM_TYPES] )(struct rbverse_node *) = {};
+
+/* Structs for passing callback data back into Ruby */
+struct rbverse_node_name_set_event {
+	VNodeID node_id;
+	const char *name;
+};
 
 
 /* --------------------------------------------------
 /*
  * Allocation function
  */
-static rbverse_NODE *
+static struct rbverse_node *
 rbverse_node_alloc( void ) {
-	rbverse_NODE *ptr = ALLOC( rbverse_NODE );
+	struct rbverse_node *ptr = ALLOC( struct rbverse_node );
 
-	ptr->id         = 0;
+	ptr->id         = ~0;
 	ptr->type       = V_NT_SYSTEM;
 	ptr->owner      = VN_OWNER_OTHER;
 	ptr->name       = Qnil;
  * GC Mark function
  */
 static void
-rbverse_node_gc_mark( rbverse_NODE *ptr ) {
+rbverse_node_gc_mark( struct rbverse_node *ptr ) {
 	if ( ptr ) {
 		rb_gc_mark( ptr->name );
 		rb_gc_mark( ptr->tag_groups );
 		rb_gc_mark( ptr->session );
 
 		/* Call the node-specific mark function if there is one */
-		if ( node_mark_funcs[ptr->type] )
+		if ( node_mark_funcs[ptr->type] ) {
+			DEBUGMSG( "  mark function 0x%p for node type %d",
+			        node_mark_funcs[ptr->type], ptr->type );
 			node_mark_funcs[ptr->type]( ptr );
+		}
 	}
 }
 
  * GC Free function
  */
 static void
-rbverse_node_gc_free( rbverse_NODE *ptr ) {
+rbverse_node_gc_free( struct rbverse_node *ptr ) {
 	if ( ptr ) {
-		if ( ptr->id ) {
+		DEBUGMSG( "Freeing node 0x%p\n", ptr );
+
+		if ( ptr->id != ~0 ) {
+			DEBUGMSG( "  removing node ID %d from node map\n", ptr->id );
 			st_delete_safe( node_map, (st_data_t*)&ptr->id, 0, Qundef );
 			/* TODO: Do I need to do st_cleanup or anything after deletion? */
 		}
 
 		/* Call the node-specific free function if there is one */
-		if ( node_free_funcs[ptr->type] )
+		if ( ptr->type < V_NT_NUM_TYPES ) {
+			DEBUGMSG( "  free function %p for node type %d\n",
+			        node_free_funcs[ptr->type], ptr->type );
 			node_free_funcs[ptr->type]( ptr );
+		}
 
-		ptr->id         = 0;
+		DEBUGMSG( "  clearing struct.\n" );
+		ptr->id         = ~0;
 		ptr->type       = V_NT_SYSTEM;
 		ptr->owner      = VN_OWNER_OTHER;
 		ptr->name       = Qnil;
 
 		xfree( ptr );
 		ptr = NULL;
+
+		DEBUGMSG( "  done.\n" );
 	}
 }
 
 /*
  * Object validity checker. Returns the data pointer.
  */
-static rbverse_NODE *
+static struct rbverse_node *
 rbverse_check_node( VALUE self ) {
 	Check_Type( self, T_DATA );
 
  * Fetch the data pointer and check it for sanity. Not static because
  * Verse::Node's children use it as well.
  */
-rbverse_NODE *
+struct rbverse_node *
 rbverse_get_node( VALUE self ) {
-	rbverse_NODE *node = rbverse_check_node( self );
+	struct rbverse_node *node = rbverse_check_node( self );
 
 	if ( !node )
 		rb_fatal( "Use of uninitialized Node." );
 }
 
 
+/*
+ * Return the Verse::Node subclass that corresponds to the specified
+ * +node_type+.
+ */
+VALUE
+rbverse_node_class_from_node_type( VNodeType node_type ) {
+	if ( node_type >= V_NT_NUM_TYPES ) return Qnil;
+	return rbverse_nodetype_to_nodeclass[ node_type ];
+}
+
 
 /*
  * Create a Verse::Node subclass from a VNodeID and a VNodeType.
 rbverse_wrap_verse_node( VNodeID node_id, VNodeType node_type, VNodeOwner owner ) {
 	VALUE node_class = rbverse_nodetype_to_nodeclass[ node_type ];
 	VALUE node = Qnil;
-	rbverse_NODE *ptr = NULL;
+	struct rbverse_node *ptr = NULL;
 
 	rbverse_log( "debug", "Wrapping a %s object around node %x",
 	             rb_class2name(node_class), node_id );
  */
 void
 rbverse_mark_node_destroyed( VALUE nodeobj ) {
-	rbverse_NODE *node = rbverse_get_node( nodeobj );
+	struct rbverse_node *node = rbverse_get_node( nodeobj );
 
 	rbverse_log_with_context( nodeobj, "debug", "Marking node %x destroyed.", node->id );
 	node->destroyed = TRUE;
  * of a session or has been destroyed.
  */
 static inline void
-rbverse_ensure_node_is_alive( rbverse_NODE *node ) {
+rbverse_ensure_node_is_alive( struct rbverse_node *node ) {
 	if ( !node )
 		rb_fatal( "node pointer was NULL!" );
 	if ( !RTEST(node->session) )
  */
 static VALUE
 rbverse_verse_node_s_allocate( VALUE klass ) {
+	if ( klass == rbverse_cVerseNode )
+		rb_raise( rb_eTypeError, "can't instantiate %s directly", rb_class2name(klass) );
 	return Data_Wrap_Struct( klass, rbverse_node_gc_mark, rbverse_node_gc_free, 0 );
 }
 
 static VALUE
 rbverse_verse_node_initialize( VALUE self ) {
 	if ( !rbverse_check_node(self) ) {
-		rbverse_NODE *node;
+		struct rbverse_node *node;
 
 		DATA_PTR( self ) = node = rbverse_node_alloc();
 
  */
 static VALUE
 rbverse_verse_node_add_observer( VALUE self, VALUE observer ) {
-	rbverse_NODE *node = rbverse_get_node( self );
+	struct rbverse_node *node = rbverse_get_node( self );
 
 	verse_send_node_subscribe( node->id );
 
  */
 static VALUE
 rbverse_verse_node_destroy( VALUE self ) {
-	rbverse_NODE *node = rbverse_get_node( self );
+	struct rbverse_node *node = rbverse_get_node( self );
 
 	rbverse_ensure_node_is_alive( node );
 	rbverse_with_session_lock( node->session, rbverse_verse_node_destroy_l, (VALUE)node->id );
  */
 static VALUE
 rbverse_verse_node_destroyed_p( VALUE self ) {
-	rbverse_NODE *node = rbverse_get_node( self );
+	struct rbverse_node *node = rbverse_get_node( self );
 	return node->destroyed ? Qtrue : Qfalse;
 }
 
  */
 static VALUE
 rbverse_verse_node_session( VALUE self ) {
-	rbverse_NODE *node = rbverse_get_node( self );
+	struct rbverse_node *node = rbverse_get_node( self );
 	return node->session;
 }
 
 
+/*
+ * call-seq:
+ *    node.id   -> Fixnum
+ *
+ * Return the node's numeric ID.
+ *
+ */
+static VALUE
+rbverse_verse_node_id( VALUE self ) {
+	struct rbverse_node *node = rbverse_get_node( self );
+	return INT2FIX( node->id );
+}
 
-/* --------------------------------------------------------------
- * Protected Instance Methods
- * -------------------------------------------------------------- */
+
+/*
+ * call-seq:
+ *    node.id = fixnum
+ *
+ * Set the node's numeric ID.
+ * 
+ * @raise [Verse::NodeError]  if the node's ID has already been set.
+ */
+static VALUE
+rbverse_verse_node_id_eq( VALUE self, VALUE newid ) {
+	struct rbverse_node *node = rbverse_get_node( self );
+
+	if ( node->id != ~0 )
+		rb_raise( rbverse_eVerseNodeError, "node's ID is already set" );
+
+	node->id = NUM2UINT( newid );
+
+	return Qtrue;
+}
+
 
 /*
  * call-seq:
  */
 static VALUE
 rbverse_verse_node_session_eq( VALUE self, VALUE session ) {
-	rbverse_NODE *node = rbverse_get_node( self );
+	struct rbverse_node *node = rbverse_get_node( self );
 
 	if ( RTEST(node->session) )
 		rb_raise( rbverse_eVerseSessionError, "%s already belongs to a session", 
 
 
 
+/* --------------------------------------------------------------
+ * Callbacks
+ * -------------------------------------------------------------- */
+
+/*
+ * Iterator body for node_name_set observers.
+ */
+static VALUE
+rbverse_cb_node_name_set_i( VALUE observer, VALUE node, int argc, VALUE *argv ) {
+	const VALUE name = argv[0];
+	VALUE cb_args = rb_ary_new2( 2 );
+
+	if ( !rb_obj_is_kind_of(observer, rbverse_mVerseNodeObserver) )
+		return Qnil;
+
+	RARRAY_PTR( cb_args )[0] = node;
+	RARRAY_PTR( cb_args )[1] = name;
+
+	rbverse_log( "debug", "Node_name_set callback: notifying observer: %s.",
+	             RSTRING_PTR(rb_inspect( observer )) );
+	return rb_funcall2( observer, rb_intern("on_node_name_set"), RARRAY_LEN(cb_args),
+	                    RARRAY_PTR(cb_args) );
+}
+
+
+/*
+ * Call the node_name_set handler after aqcuiring the GVL.
+ */
+static void *
+rbverse_cb_node_name_set_body( void *ptr ) {
+	struct rbverse_node_name_set_event *event = (struct rbverse_node_name_set_event *)ptr;
+	const VALUE node = rbverse_lookup_verse_node( event->node_id );
+	VALUE name = rb_str_new2( event->name );
+	VALUE observers;
+
+	if ( RTEST(node) ) {
+		rbverse_log( "info", "Got node name '%s' for %p.", event->name,
+		             RSTRING_PTR(rb_inspect(node)) );
+
+		observers = rb_funcall( node, rb_intern("observers"), 0 );
+		rb_block_call( observers, rb_intern("each"), 1, &name, rbverse_cb_node_name_set_i, node );
+
+	} else {
+		rbverse_log( "info", "Got node name for a node we haven't loaded (%d)", event->node_id );
+	}
+
+	return NULL;
+}
+
+
+/* 
+ * Callback for the 'node_name_set' command
+ */
+static void
+rbverse_cb_node_name_set( void *unused, VNodeID node_id, const char *name ) {
+	struct rbverse_node_name_set_event event;
+
+	event.node_id = node_id;
+	event.name = name;
+
+	DEBUGMSG( " Acquiring GVL for 'node_name_set' event.\n" );
+	fflush( stdout );
+
+	rb_thread_call_with_gvl( rbverse_cb_node_name_set_body, (void *)&event );
+}
+
+
+// static void
+// rbverse_cb_node_tag_group_create( void *unused ) {}
+// 
+// static void
+// rbverse_cb_node_tag_group_destroy( void *unused ) {}
+// 
+// static void
+// rbverse_cb_node_tag_group_subscribe( void *unused ) {}
+// 
+// static void
+// rbverse_cb_node_tag_group_unsubscribe( void *unused ) {}
+// 
+// static void
+// rbverse_cb_node_tag_create( void *unused ) {}
+// 
+// static void
+// rbverse_cb_node_tag_destroy( void *unused ) {}
+
+
+
 /*
  * Verse::Node class
  */
 	rbverse_mVerse = rb_define_module( "Verse" );
 #endif
 
+	/* Related modules */
+	rbverse_mVerseNodeObserver = rb_define_module_under( rbverse_mVerse, "NodeObserver" );
+
+	/* VNodeID -> Ruby object lookup table */
 	node_map = st_init_numtable();
 
 	rbverse_cVerseNode = rb_define_class_under( rbverse_mVerse, "Node", rb_cObject );
 
 	/* Class methods */
 	rb_define_alloc_func( rbverse_cVerseNode, rbverse_verse_node_s_allocate );
-	rb_undef_method( CLASS_OF(rbverse_cVerseNode), "new" );
 
 	/* Initializer */
 	rb_define_method( rbverse_cVerseNode, "initialize", rbverse_verse_node_initialize, 0 );
 
 	rb_define_method( rbverse_cVerseNode, "session", rbverse_verse_node_session, 0 );
 
+	rb_define_method( rbverse_cVerseNode, "id", rbverse_verse_node_id, 0 );
+	rb_define_method( rbverse_cVerseNode, "id=", rbverse_verse_node_id_eq, 1 );
+
 	/* Protected instance methods */
 	rb_define_protected_method( rbverse_cVerseNode, "session=", rbverse_verse_node_session_eq, 1 );
 
 	rbverse_init_verse_curvenode();
 	rbverse_init_verse_audionode();
 
-	// verse_callback_set( verse_send_node_name_set, rbverse_node_name_set_callback, NULL );
-	// verse_callback_set( verse_send_tag_group_create, rbverse_tag_group_create_callback, NULL );
-	// verse_callback_set( verse_send_tag_group_destroy, rbverse_tag_group_destroy_callback, NULL );
-	// verse_callback_set( verse_send_tag_group_subscribe, rbverse_tag_group_subscribe_callback, NULL );
-	// verse_callback_set( verse_send_tag_group_unsubscribe, rbverse_tag_group_unsubscribe_callback, NULL );
-	// verse_callback_set( verse_send_tag_create, rbverse_tag_create_callback, NULL );
-	// verse_callback_set( verse_send_tag_destroy, rbverse_tag_destroy_callback, NULL );
+	// DEBUGMSG( "Free function for ObjectNode (%d) is: %p\n", V_NT_OBJECT, node_free_funcs[V_NT_OBJECT] );
+	// DEBUGMSG( "Free function for GeometryNode (%d) is: %p\n", V_NT_GEOMETRY, node_free_funcs[V_NT_GEOMETRY] );
+	// DEBUGMSG( "Free function for MaterialNode (%d) is: %p\n", V_NT_MATERIAL, node_free_funcs[V_NT_MATERIAL] );
+	// DEBUGMSG( "Free function for TextNode (%d) is: %p\n", V_NT_TEXT, node_free_funcs[V_NT_TEXT] );
+	// DEBUGMSG( "Free function for BitmapNode (%d) is: %p\n", V_NT_BITMAP, node_free_funcs[V_NT_BITMAP] );
+	// DEBUGMSG( "Free function for CurveNode (%d) is: %p\n", V_NT_CURVE, node_free_funcs[V_NT_CURVE] );
+	// DEBUGMSG( "Free function for AudioNode (%d) is: %p\n", V_NT_AUDIO, node_free_funcs[V_NT_AUDIO] );
+
+	verse_callback_set( verse_send_node_name_set, rbverse_cb_node_name_set, NULL );
+	// verse_callback_set( verse_send_tag_group_create, rbverse_cb_node_tag_group_create, NULL );
+	// verse_callback_set( verse_send_tag_group_destroy, rbverse_cb_node_tag_group_destroy, NULL );
+	// verse_callback_set( verse_send_tag_group_subscribe, rbverse_cb_node_tag_group_subscribe, NULL );
+	// verse_callback_set( verse_send_tag_group_unsubscribe, rbverse_cb_node_tag_group_unsubscribe, NULL );
+	// verse_callback_set( verse_send_tag_create, rbverse_cb_node_tag_create, NULL );
+	// verse_callback_set( verse_send_tag_destroy, rbverse_cb_node_tag_destroy, NULL );
 }
 

ext/nodeclass.template

  * Mark the <%= nodetype %> part of a node.
  */
 static void
-rbverse_<%= nodetype %>node_gc_mark( rbverse_NODE *ptr ) {
+rbverse_<%= nodetype %>node_gc_mark( struct rbverse_node *ptr ) {
 	if ( ptr ) {
 		/* TODO: Mark child-specific data */
 	}
  * Free the <%= nodetype %> part of a node.
  */
 static void
-rbverse_<%= nodetype %>node_gc_free( rbverse_NODE *ptr ) {
+rbverse_<%= nodetype %>node_gc_free( struct rbverse_node *ptr ) {
 	if ( ptr ) {
 		/* TODO: Free child-specific data */
 	}
  */
 static VALUE
 rbverse_verse_<%= nodetype %>node_initialize( VALUE self ) {
-	rbverse_NODE *ptr;
+	struct rbverse_node *ptr;
 
 	rb_call_super( 0, NULL );
 
 	ptr = rbverse_get_node( self );
+	ptr->type = V_NT_<%= nodetype.upcase %>;
 
 	/* TODO: Initialize <%= nodetype %>-specific instance data. */
 
  * Mark the object part of a node.
  */
 static void
-rbverse_objectnode_gc_mark( rbverse_NODE *ptr ) {
+rbverse_objectnode_gc_mark( struct rbverse_node *ptr ) {
 	if ( ptr ) {
-		/* TODO: Mark child-specific data */
+		rb_gc_mark( ptr->object.links );
+		rb_gc_mark( ptr->object.links );
+		rb_gc_mark( ptr->object.transform );
+		rb_gc_mark( ptr->object.light );
+		rb_gc_mark( ptr->object.method_groups );
+		rb_gc_mark( ptr->object.animations );
 	}
 }
 
  * Free the object part of a node.
  */
 static void
-rbverse_objectnode_gc_free( rbverse_NODE *ptr ) {
+rbverse_objectnode_gc_free( struct rbverse_node *ptr ) {
 	if ( ptr ) {
-		/* TODO: Free child-specific data */
+		ptr->object.links         = Qnil;
+		ptr->object.transform     = Qnil;
+		ptr->object.light         = Qnil;
+		ptr->object.method_groups = Qnil;
+		ptr->object.animations    = Qnil;
 	}
 }
 
  */
 static VALUE
 rbverse_verse_objectnode_initialize( VALUE self ) {
-	rbverse_NODE *ptr;
+	struct rbverse_node *ptr;
 
 	rb_call_super( 0, NULL );
 
 	ptr = rbverse_get_node( self );
+	ptr->type = V_NT_OBJECT;
 
-	/* TODO: Initialize object-specific instance data. */
+	ptr->object.links         = Qnil;
+	ptr->object.transform     = Qnil;
+	ptr->object.light         = Qnil;
+	ptr->object.method_groups = Qnil;
+	ptr->object.animations    = Qnil;
 
 	return self;
 }
 #include "verse_ext.h"
 
 VALUE rbverse_cVerseSession;
+VALUE rbverse_mVerseSessionObserver;
 
 static VALUE rbverse_session_mutex;
 static st_table *session_table;
 
+/* Structs for passing callback data back into Ruby */
+struct rbverse_node_create_event {
+	VNodeID node_id;
+	VNodeType type;
+	VNodeOwner owner;
+};
+
+struct rbverse_connect_accept_event {
+	VNodeID		    avatar;
+	const char	    *address;
+	uint8		    *hostid;
+};
+
+
 
 /* --------------------------------------------------
  *	Memory-management functions
 /*
  * Allocation function
  */
-static rbverse_SESSION *
+static struct rbverse_session *
 rbverse_session_alloc( void ) {
-	rbverse_SESSION *ptr = ALLOC( rbverse_SESSION );
+	struct rbverse_session *ptr = ALLOC( struct rbverse_session );
 
-	ptr->id        = NULL;
-	ptr->address   = Qnil;
-	ptr->self      = Qnil;
+	ptr->id                = NULL;
+	ptr->address           = Qnil;
+	ptr->create_callbacks  = Qnil;
+	ptr->destroy_callbacks = Qnil;
 
 	rbverse_log( "debug", "allocated a rbverse_SESSION <%p>", ptr );
 	return ptr;
  * GC Mark function
  */
 static void
-rbverse_session_gc_mark( rbverse_SESSION *ptr ) {
+rbverse_session_gc_mark( struct rbverse_session *ptr ) {
 	if ( ptr ) {
 		rb_gc_mark( ptr->address );
-		/* Don't need to mark ->self */
+		rb_gc_mark( ptr->create_callbacks );
+		rb_gc_mark( ptr->destroy_callbacks );
 	}
 }
 
  * GC Free function
  */
 static void
-rbverse_session_gc_free( rbverse_SESSION *ptr ) {
+rbverse_session_gc_free( struct rbverse_session *ptr ) {
 	if ( ptr ) {
 		/* TODO: terminate the session and remove it from the session table if it's connected */
 		if ( ptr->id ) {
 			verse_session_destroy( ptr->id );
 		}
 
-		ptr->id         = NULL;
-		ptr->address    = Qnil;
-		ptr->self       = Qnil;
+		ptr->id                = NULL;
+		ptr->address           = Qnil;
+		ptr->create_callbacks  = Qnil;
+		ptr->destroy_callbacks = Qnil;
 
 		xfree( ptr );
 		ptr = NULL;
 /*
  * Object validity checker. Returns the data pointer.
  */
-static rbverse_SESSION *
+static struct rbverse_session *
 check_session( VALUE self ) {
 	Check_Type( self, T_DATA );
 
 /*
  * Fetch the data pointer and check it for sanity.
  */
-static rbverse_SESSION *
+static struct rbverse_session *
 rbverse_get_session( VALUE self ) {
-	rbverse_SESSION *session = check_session( self );
+	struct rbverse_session *session = check_session( self );
 
 	if ( !session )
 		rb_fatal( "Use of uninitialized Session." );
  */
 VALUE
 rbverse_with_session_lock( VALUE sessionobj, VALUE (*func)(ANYARGS), VALUE arg ) {
-	rbverse_SESSION *session = rbverse_get_session( sessionobj );
+	struct rbverse_session *session = rbverse_get_session( sessionobj );
 	VALUE rval = Qnil;
 
 	rbverse_log( "debug", "About to acquire the session mutex for session %d", session->id );
 VALUE
 rbverse_verse_session_from_vsession( VSession id, VALUE address ) {
 	VALUE session_obj = rb_class_new_instance( 1, &address, rbverse_cVerseSession );
-	rbverse_SESSION *session = check_session( session_obj );
+	struct rbverse_session *session = check_session( session_obj );
 
 	st_insert( session_table, (st_data_t)id, (st_data_t)session_obj );
 	session->id = id;
 }
 
 
-/* Mutex body for rbverse_verse_session_s_synchronize */
-static VALUE
-rbverse_verse_session_s_synchronize_body( VALUE block ) {
-	return rb_funcall( block, rb_intern("call"), 0 );
-}
-
-
-/*
- * call-seq:
- *    Verse::Session.synchronize { ... }
- * 
- * Call the block after acquiring the global session lock.
- * 
- */
-static VALUE
-rbverse_verse_session_s_synchronize( int argc, VALUE *argv, VALUE klass ) {
-	VALUE block;
-
-	if ( !rb_block_given_p() )
-		rb_raise( rb_eLocalJumpError, "no block given" );
-
-	block = rb_block_proc();
-
-	return rb_mutex_synchronize( rbverse_session_mutex, rbverse_verse_session_s_synchronize_body,
-	                             block );
-}
-
-
 /*
  * Iterator body for rbverse_verse_session_s_all_connected
  */
 static VALUE
 rbverse_verse_session_initialize( int argc, VALUE *argv, VALUE self ) {
 	if ( !check_session(self) ) {
-		rbverse_SESSION *session;
+		struct rbverse_session *session;
 		VALUE address = Qnil;
 
 		DATA_PTR( self ) = session = rbverse_session_alloc();
 			session->address = address;
 		}
 
-		session->self = self;
+		/* Creation callbacks have a queue per node class, so create them now
+		 * to avoid the race condition in on-demand creation. */
+		session->create_callbacks = rb_hash_new();
+		rb_hash_aset( session->create_callbacks, rbverse_cVerseAudioNode,    rb_ary_new() );
+		rb_hash_aset( session->create_callbacks, rbverse_cVerseBitmapNode,   rb_ary_new() );
+		rb_hash_aset( session->create_callbacks, rbverse_cVerseCurveNode,    rb_ary_new() );
+		rb_hash_aset( session->create_callbacks, rbverse_cVerseGeometryNode, rb_ary_new() );
+		rb_hash_aset( session->create_callbacks, rbverse_cVerseMaterialNode, rb_ary_new() );
+		rb_hash_aset( session->create_callbacks, rbverse_cVerseObjectNode,   rb_ary_new() );
+		rb_hash_aset( session->create_callbacks, rbverse_cVerseTextNode,     rb_ary_new() );
+
+		/* The destruction callbacks are keyed by the node requesting destruction,
+		 * so it can be just a simple hash */
+		session->destroy_callbacks = rb_hash_new();
 
 		rb_call_super( 0, NULL );
 	} else {
  */
 static VALUE
 rbverse_verse_session_address( VALUE self ) {
-	rbverse_SESSION *session = rbverse_get_session( self );
+	struct rbverse_session *session = rbverse_get_session( self );
 	return rb_obj_clone( session->address );
 }
 
  */
 static VALUE
 rbverse_verse_session_address_eq( VALUE self, VALUE address ) {
-	rbverse_SESSION *session = rbverse_get_session( self );
+	struct rbverse_session *session = rbverse_get_session( self );
 
 	SafeStringValue( address );
 	session->address = rb_obj_clone( address );
  */
 static VALUE
 rbverse_verse_session_connected_p( VALUE self ) {
-	rbverse_SESSION *session = rbverse_get_session( self );
+	struct rbverse_session *session = rbverse_get_session( self );
 	return ( session->id ? Qtrue : Qfalse );
 }
 
 static VALUE
 rbverse_verse_session_connect_body( VALUE args ) {
 	VALUE self, name, pass, expected_host_id;
-	rbverse_SESSION *session;
+	struct rbverse_session *session;
 	VSession session_id;
 	const char *name_str, *pass_str, *addr_str;
 	const uint8 *hostid = NULL;
  */
 static VALUE
 rbverse_verse_session_connect( int argc, VALUE *argv, VALUE self ) {
-	rbverse_SESSION *session = rbverse_get_session( self );
+	struct rbverse_session *session = rbverse_get_session( self );
 	VALUE args, name, pass, expected_host_id = Qnil;
 	VALUE rval = Qnil;
 
 	if ( !RTEST(session->address) )
-		rb_raise( rb_eRuntimeError, "No address set." );
+		rb_raise( rbverse_eVerseSessionError, "No address set." );
 	if ( session->id )
-		rb_raise( rb_eRuntimeError, "Session already established." );
+		rb_raise( rbverse_eVerseSessionError, "Session already established." );
 
 	rb_scan_args( argc, argv, "21", &name, &pass, &expected_host_id );
 
 	rbverse_log_with_context( self, "debug", "Acquiring session lock for new session." );
 	args = rb_ary_new3( 4, self, name, pass, expected_host_id );
-	rval = rb_mutex_synchronize( rbverse_session_mutex, rbverse_verse_session_connect_body, args );
+	rval = rbverse_with_session_lock( self, rbverse_verse_session_connect_body, args );
 	rbverse_log_with_context( self, "debug", "  releasing session lock." );
 
 	return rval;
  */
 static VALUE
 rbverse_verse_session_terminate( VALUE self, VALUE message ) {
-	rbverse_SESSION *session = rbverse_get_session( self );
+	struct rbverse_session *session = rbverse_get_session( self );
 	const char *msg;
 
 	SafeStringValue( message );
  * session lock.
  */
 static VALUE
-rbverse_verse_session_subscribe_node_index_l( VALUE typemask ) {
+rbverse_verse_session_subscribe_to_node_index_l( VALUE typemask ) {
 	verse_send_node_index_subscribe( (uint32)typemask );
 	return Qtrue;
 }
 
 /*
  * call-seq:
- *    session.subscribe_node_index( *node_classes )
+ *    session.subscribe_to_node_index( *node_classes )
  *
  * Subscribe to creation and destruction events for the specified +node_classes+. Node 
  * creation will be sent to the Verse::SessionObservers via their #on_node_create method,
  *                                     from all node types.
  */
 static VALUE
-rbverse_verse_session_subscribe_node_index( int argc, VALUE *argv, VALUE self ) {
+rbverse_verse_session_subscribe_to_node_index( int argc, VALUE *argv, VALUE self ) {
+	struct rbverse_session *session = rbverse_get_session( self );
 	VALUE node_classes = Qnil;
 	uint32 typemask = ~0;
 	int i;
 
+	if ( !session->id )
+		rb_raise( rbverse_eVerseSessionError, "can't subscribe an unconnected session" );
+
 	if ( rb_scan_args(argc, argv, "0*", &node_classes) ) {
 		for ( i = 0; i < RARRAY_LEN(node_classes); i++ ) {
 			VALUE typenum = rb_const_get( RARRAY_PTR(node_classes)[i], rb_intern("TYPE_NUMBER") );
 		}
 	}
 
-	rbverse_with_session_lock( self, rbverse_verse_session_subscribe_node_index_l, (VALUE)typemask );
+	rbverse_with_session_lock( self, rbverse_verse_session_subscribe_to_node_index_l,
+	                           (VALUE)typemask );
 
 	return INT2FIX( typemask );
 }
 
 
+/*
+ * Synchronized portion of rbverse_verse_session_create_node() 
+ */
+static VALUE
+rbverse_verse_session_create_node_l( VALUE nodeclass ) {
+	const VNodeType nodetype = FIX2UINT( rb_const_get(nodeclass, rb_intern( "TYPE_NUMBER" )) );
+	verse_send_node_create( ~0, nodetype, 0 );
+	return Qtrue;
+}
+
+/*
+ * call-seq:
+ *    session.create_node( nodeclass ) {|node| ... }
+ *
+ * Ask the server to create a new node of the specified +nodeclass+. If the node is successfully 
+ * created, the provided block will be called with the new node object. Note that this happens
+ * asynchronously, so you shouldn't count on the callback being run when this method returns.
+ * 
+ * @yield [node]  called when the node is created with the node object
+ * @example Creating a new ObjectNode
+ *     objectnode = nil
+ *     session.create_node( Verse::ObjectNode ) {|node| objectnode = node }
+ *     Verse::Session.update until objectnode
+ *   
+ */
+static VALUE
+rbverse_verse_session_create_node( int argc, VALUE *argv, VALUE self ) {
+	struct rbverse_session *session = rbverse_get_session( self );
+	VALUE nodeclass, callback, callback_queue;
+
+	if ( !session->id )
+		rb_raise( rbverse_eVerseSessionError, "can't create a node via an unconnected session" );
+
+	rb_scan_args( argc, argv, "1&", &nodeclass, &callback );
+	callback_queue = rb_hash_aref( session->create_callbacks, nodeclass );
+
+	Check_Type( nodeclass, T_CLASS );
+
+	if ( !RTEST(callback) )
+		callback = rb_block_proc();
+	if ( !RTEST(callback_queue) )
+		rb_raise( rbverse_eVerseSessionError,
+		          "don't know how to create %s objects (no callback queue)", 
+		          rb_class2name(nodeclass) );
+
+	/* Add the callback to the queue. This isn't done inside the lock as
+	 * we don't really care if the nodes are created strictly in the order
+	 * in which they're created. */
+	rb_ary_push( callback_queue, callback );
+	return rbverse_with_session_lock( self, rbverse_verse_session_create_node_l, nodeclass );
+}
+
+
+/*
+ * call-seq:
+ *    session.destroy_node( node ) {|node| ... }
+ *
+ * Ask the server to destroy the specified +node+. If the node is successfully destroyed,
+ * the provided block is called with the now-destroyed node and the session's observers 
+ * are notified via Verse::SessionObserver#on_node_destroy.
+ * 
+ * @yield [node]  called when the node is destroyed.
+ * 
+ * @example Remove the node from the list of active objects once it's destroyed
+ *     active_nodes = [ objectnode ]
+ *     session.destroy_node( objectnode ) {|node| active_nodes.delete(node) }
+ *     Verse::Session.update while active_nodes.include?( objectnode )
+ */
+static VALUE
+rbverse_verse_session_destroy_node( int argc, VALUE *argv, VALUE self ) {
+	struct rbverse_session *session = rbverse_get_session( self );
+	VALUE nodeclass, callback, callback_queue;
+
+	if ( !session->id )
+		rb_raise( rbverse_eVerseSessionError, "can't create a node via an unconnected session" );
+
+	rb_scan_args( argc, argv, "1&", &nodeclass, &callback );
+	callback_queue = rb_hash_aref( session->create_callbacks, nodeclass );
+
+	Check_Type( nodeclass, T_CLASS );
+
+	if ( !RTEST(callback) )
+		callback = rb_block_proc();
+	if ( !RTEST(callback_queue) )
+		rb_raise( rbverse_eVerseSessionError,
+		          "don't know how to create %s objects (no callback queue)", 
+		          rb_class2name(nodeclass) );
+
+	/* Add the callback to the queue. This isn't done inside the lock as
+	 * we don't really care if the nodes are created strictly in the order
+	 * in which they're created. */
+	rb_ary_push( callback_queue, callback );
+	return rbverse_with_session_lock( self, rbverse_verse_session_create_node_l, nodeclass );
+}
+
+
 /* --------------------------------------------------------------
  * Callbacks
  * -------------------------------------------------------------- */
  * Iterator body for connect_accept observers.
  */
 static VALUE
-rbverse_cb_connect_accept_i( VALUE observer, VALUE cb_args ) {
+rbverse_cb_session_connect_accept_i( VALUE observer, VALUE cb_args ) {
 	if ( !rb_obj_is_kind_of(observer, rbverse_mVerseSessionObserver) )
 		return Qnil;
 
 
 /*
  * Ruby handler for the 'connect_accept' message; called after re-establishing the GVL
- * from rbverse_cb_connect_accept().
+ * from rbverse_cb_session_connect_accept().
  */
 static void *
-rbverse_cb_connect_accept_body( void *ptr ) {
-	rbverse_CONNECT_ACCEPT_EVENT *event = (rbverse_CONNECT_ACCEPT_EVENT *)ptr;
+rbverse_cb_session_connect_accept_body( void *ptr ) {
+	struct rbverse_connect_accept_event *event = (struct rbverse_connect_accept_event *)ptr;
 	const VALUE cb_args = rb_ary_new2( 3 );
 	VALUE session = rbverse_get_current_session();
 	VALUE observers = rb_funcall( session, rb_intern("observers"), 0 );
 	RARRAY_PTR(cb_args)[1] = rb_str_new2( event->address );
 	RARRAY_PTR(cb_args)[2] = rbverse_host_id2str( event->hostid );
 
-	rb_block_call( observers, rb_intern("each"), 0, 0, rbverse_cb_connect_accept_i, cb_args );
+	rb_block_call( observers, rb_intern("each"), 0, 0, rbverse_cb_session_connect_accept_i, cb_args );
 
 	return NULL;
 }
  * Verse callback for the 'connect_accept' message
  */
 static void
-rbverse_cb_connect_accept( void *unused, VNodeID avatar, const char *address, uint8 *host_id ) {
+rbverse_cb_session_connect_accept( void *unused, VNodeID avatar, const char *address, uint8 *host_id ) {
 	struct rbverse_connect_accept_event event;
 
 	event.avatar  = avatar;
 	event.address = address;
 	event.hostid  = host_id;
 
-	rb_thread_call_with_gvl( rbverse_cb_connect_accept_body, (void *)&event );
+	rb_thread_call_with_gvl( rbverse_cb_session_connect_accept_body, (void *)&event );
 }
 
 
  * Iterator body for connect_terminate observers.
  */
 static VALUE
-rbverse_cb_connect_terminate_i( VALUE observer, VALUE cb_args ) {
+rbverse_cb_session_connect_terminate_i( VALUE observer, VALUE cb_args ) {
 	if ( !rb_obj_is_kind_of(observer, rbverse_mVerseSessionObserver) )
 		return Qnil;
 
 	rbverse_log( "debug", "Connect_terminate callback: notifying observer: %s.",
 	             RSTRING_PTR(rb_inspect( observer )) );
-	return rb_funcall2( observer, rb_intern("on_connect_terminate"), 
+	return rb_funcall2( observer, rb_intern("on_connect_terminate"),
 	                    RARRAY_LEN(cb_args), RARRAY_PTR(cb_args) );
 }
 
  * Call the connect_terminate handler after aqcuiring the GVL.
  */
 static void *
-rbverse_cb_connect_terminate_body( void *ptr ) {
+rbverse_cb_session_connect_terminate_body( void *ptr ) {
 	const char **args = (const char **)ptr;
 	const VALUE cb_args = rb_ary_new2( 2 );
 	VALUE session, observers;
 	RARRAY_PTR(cb_args)[0] = rb_str_new2( args[0] );
 	RARRAY_PTR(cb_args)[1] = rb_str_new2( args[1] );
 
-	rb_block_call( observers, rb_intern("each"), 0, 0, rbverse_cb_connect_terminate_i, cb_args );
+	rb_block_call( observers, rb_intern("each"), 0, 0, rbverse_cb_session_connect_terminate_i, cb_args );
 
 	return NULL;
 }
  * Callback for the 'connect_terminate' command.
  */
 static void
-rbverse_cb_connect_terminate( void *unused, const char *address, const char *msg ) {
+rbverse_cb_session_connect_terminate( void *unused, const char *address, const char *msg ) {
 	const char *(args[2]) = { address, msg };
-	printf( " Acquiring GVL for 'connect_terminate' event.\n" );
+	DEBUGMSG( " Acquiring GVL for 'connect_terminate' event.\n" );
 	fflush( stdout );
-	rb_thread_call_with_gvl( rbverse_cb_connect_terminate_body, args );
+	rb_thread_call_with_gvl( rbverse_cb_session_connect_terminate_body, args );
 }
 
 
  * Iterator body for node_create observers.
  */
 static VALUE
-rbverse_cb_node_create_i( VALUE observer, VALUE node ) {
+rbverse_cb_session_node_create_i( VALUE observer, VALUE node ) {
 	if ( !rb_obj_is_kind_of(observer, rbverse_mVerseSessionObserver) )
 		return Qnil;
 
  * Call the node_create handler after aqcuiring the GVL.
  */
 static void *
-rbverse_cb_node_create_body( void *ptr ) {
-	rbverse_NODE_CREATE_EVENT *event = (rbverse_NODE_CREATE_EVENT *)ptr;
-	VALUE node, session, observers;
+rbverse_cb_session_node_create_body( void *ptr ) {
+	struct rbverse_node_create_event *event = (struct rbverse_node_create_event *)ptr;
+	VALUE self, node, cb_queue, callback, observers;
+	struct rbverse_session *session = NULL;
 
-	session   = rbverse_get_current_session();
-	observers = rb_funcall( session, rb_intern("observers"), 0 );
 	node      = rbverse_wrap_verse_node( event->node_id, event->type, event->owner );
+	self      = rbverse_get_current_session();
+	session   = rbverse_get_session( self );
+	cb_queue  = rb_hash_aref( session->create_callbacks, rb_class_of(node) );
+	observers = rb_funcall( self, rb_intern("observers"), 0 );
 
-	rb_funcall3( node, rb_intern("session="), 1, &session );
-	rb_block_call( observers, rb_intern("each"), 0, 0, rbverse_cb_node_create_i, node );
+	/* Set the node's session */
+	rb_funcall3( node, rb_intern("session="), 1, &self );
+
+	/* If this session was the node's creator, and there's a creation
+	 * callback queue for the class of node that was created, and there's a callback in
+	 * the queue, shift it off and call it with the node object. */
+	if ( event->owner == VN_OWNER_MINE &&
+	     RTEST(cb_queue) &&
+	     RTEST(callback = rb_ary_shift(cb_queue)) )
+	{
+		rbverse_log_with_context( self, "debug", "calling create callback %p for node %p",
+		                          RSTRING_PTR(rb_inspect( callback )),
+		                          RSTRING_PTR(rb_inspect( node )) );
+		rb_funcall3( callback, rb_intern("call"), 1, &node );
+	} else {
+		rbverse_log_with_context( self, "debug", "no creation callback for node %p",
+		                          RSTRING_PTR(rb_inspect( node )) );
+	}
+
+	/* Now notify the session's observers that a node was created */
+	rb_block_call( observers, rb_intern("each"), 0, 0, rbverse_cb_session_node_create_i, node );
 
 	return NULL;
 }
  * Callback for the 'node_create' command.
  */
 static void
-rbverse_cb_node_create( void *unused, VNodeID node_id, VNodeType type, VNodeOwner owner ) {
-	rbverse_NODE_CREATE_EVENT event;
+rbverse_cb_session_node_create( void *unused, VNodeID node_id, VNodeType type, VNodeOwner owner ) {
+	struct rbverse_node_create_event event;
 
 	event.node_id = node_id;
 	event.type    = type;
 	event.owner   = owner;
 
-	printf( " Acquiring GVL for 'node_create' event.\n" );
+	DEBUGMSG( " Acquiring GVL for 'node_create' event.\n" );
 	fflush( stdout );
 
-	rb_thread_call_with_gvl( rbverse_cb_node_create_body, (void *)&event );
+	rb_thread_call_with_gvl( rbverse_cb_session_node_create_body, (void *)&event );
 }
 
 
  * Iterator body for node_destroy observers.
  */
 static VALUE
-rbverse_cb_node_destroy_i( VALUE observer, VALUE node ) {
+rbverse_cb_session_node_destroy_i( VALUE observer, VALUE node ) {
 	if ( !rb_obj_is_kind_of(observer, rbverse_mVerseSessionObserver) )
 		return Qnil;
 
  * Call the node_destroy handler after aqcuiring the GVL.
  */
 static void *
-rbverse_cb_node_destroy_body( void *ptr ) {
+rbverse_cb_session_node_destroy_body( void *ptr ) {
 	VNodeID *node_id = (VNodeID *)ptr;
 	VALUE node;
 
 		VALUE session = rb_funcall( node, rb_intern("session"), 0 );
 		VALUE observers = rb_funcall( session, rb_intern("observers"), 0 );
 		rbverse_mark_node_destroyed( node );
-		rb_block_call( observers, rb_intern("each"), 0, 0, rbverse_cb_node_destroy_i, node );
+		rb_block_call( observers, rb_intern("each"), 0, 0, rbverse_cb_session_node_destroy_i, node );
 	} else {
 		rbverse_log( "info", "destroy event received for unwrapped node %x", node_id );
 	}
  * Callback for the 'node_destroy' command.
  */
 static void
-rbverse_cb_node_destroy( void *unused, VNodeID node_id ) {
-	printf( " Acquiring GVL for 'node_destroy' event.\n" );
+rbverse_cb_session_node_destroy( void *unused, VNodeID node_id ) {
+	DEBUGMSG( " Acquiring GVL for 'node_destroy' event.\n" );
 	fflush( stdout );
 
-	rb_thread_call_with_gvl( rbverse_cb_node_destroy_body, (void *)&node_id );
+	rb_thread_call_with_gvl( rbverse_cb_session_node_destroy_body, (void *)&node_id );
 }
 
 
 	rbverse_mVerse = rb_define_module( "Verse" );
 #endif
 
+	/* Related modules */
+	rbverse_mVerseSessionObserver = rb_define_module_under( rbverse_mVerse, "SessionObserver" );
+
 	/* Class methods */
 	rbverse_cVerseSession = rb_define_class_under( rbverse_mVerse, "Session", rb_cObject );
 	rb_include_module( rbverse_cVerseSession, rbverse_mVerseLoggable );
 
 	rb_define_alloc_func( rbverse_cVerseSession, rbverse_verse_session_s_allocate );
 
-	rb_define_singleton_method( rbverse_cVerseSession, "update", rbverse_verse_session_s_update, -1 );
+	rb_define_singleton_method( rbverse_cVerseSession, "update", rbverse_verse_session_s_update,
+	                            -1 );
 	rb_define_alias( rb_singleton_class(rbverse_cVerseSession), "callback_update", "update" );
 
 	rb_attr( rb_singleton_class(rbverse_cVerseSession), rb_intern("mutex"), Qtrue, Qfalse, Qtrue );
 	rb_iv_set( rbverse_cVerseSession, "@mutex", rbverse_session_mutex );
-	rb_define_singleton_method( rbverse_cVerseSession, "synchronize",
-	                            rbverse_verse_session_s_synchronize, -1 );
 	rb_define_singleton_method( rbverse_cVerseSession, "all_connected",
 	                            rbverse_verse_session_s_all_connected, 0 );
 
 	rb_define_method( rbverse_cVerseSession, "connect", rbverse_verse_session_connect, -1 );
 	rb_define_method( rbverse_cVerseSession, "terminate", rbverse_verse_session_terminate, 1 );
 
-	rb_define_method( rbverse_cVerseSession, "subscribe_node_index",
-	                  rbverse_verse_session_subscribe_node_index, -1 );
+	rb_define_method( rbverse_cVerseSession, "subscribe_to_node_index",
+	                  rbverse_verse_session_subscribe_to_node_index, -1 );
+	rb_define_method( rbverse_cVerseSession, "create_node", rbverse_verse_session_create_node, -1 );
+	rb_define_method( rbverse_cVerseSession, "destroy_node", rbverse_verse_session_destroy_node,
+	                  -1 );
 
 	// tag_group_create(VNodeID node_id, uint16 group_id, const char *name);
 	// tag_group_destroy(VNodeID node_id, uint16 group_id);
 
 	// node_name_set(VNodeID node_id, const char *name);
 
-	verse_callback_set( verse_send_connect_accept, rbverse_cb_connect_accept, NULL );
-	verse_callback_set( verse_send_connect_terminate, rbverse_cb_connect_terminate, NULL );
-	verse_callback_set( verse_send_node_create, rbverse_cb_node_create, NULL );
-	verse_callback_set( verse_send_node_destroy, rbverse_cb_node_destroy, NULL );
+	verse_callback_set( verse_send_connect_accept, rbverse_cb_session_connect_accept, NULL );
+	verse_callback_set( verse_send_connect_terminate, rbverse_cb_session_connect_terminate, NULL );
+	verse_callback_set( verse_send_node_create, rbverse_cb_session_node_create, NULL );
+	verse_callback_set( verse_send_node_destroy, rbverse_cb_session_node_destroy, NULL );
 }
 
  * Mark the text part of a node.
  */
 static void
-rbverse_textnode_gc_mark( rbverse_NODE *ptr ) {
+rbverse_textnode_gc_mark( struct rbverse_node *ptr ) {
 	if ( ptr ) {
 		/* TODO: Mark child-specific data */
 	}
  * Free the text part of a node.
  */
 static void
-rbverse_textnode_gc_free( rbverse_NODE *ptr ) {
+rbverse_textnode_gc_free( struct rbverse_node *ptr ) {
 	if ( ptr ) {
 		/* TODO: Free child-specific data */
 	}
  */
 static VALUE
 rbverse_verse_textnode_initialize( VALUE self ) {
-	rbverse_NODE *ptr;
+	struct rbverse_node *ptr;
 
 	rb_call_super( 0, NULL );
 
 	ptr = rbverse_get_node( self );
+	ptr->type = V_NT_TEXT;
 
 	/* TODO: Initialize text-specific instance data. */
 
 	/* Class methods */
 	rbverse_cVerseTextNode = rb_define_class_under( rbverse_mVerse, "TextNode", rbverse_cVerseNode );
 
+    /* Constants */
+	rb_define_const( rbverse_cVerseTextNode, "TYPE_NUMBER", INT2FIX(V_NT_TEXT) );
+
 	/* Initializer */
 	rb_define_method( rbverse_cVerseTextNode, "initialize", rbverse_verse_textnode_initialize, 0 );
 
 VALUE rbverse_mVerseObserver;
 VALUE rbverse_mVersePingObserver;
 VALUE rbverse_mVerseConnectionObserver;
-VALUE rbverse_mVerseSessionObserver;
 
 VALUE rbverse_eVerseError;
 VALUE rbverse_eVerseConnectError;
 
 
 /*
- * Call the connect handler after aqcuiring the GVL.
+ * Call the connect handler after acquiring the GVL.
  */
 static void *
 rbverse_cb_connect_body( void *ptr ) {
  */
 static void
 rbverse_cb_connect( void *unused, const char *name, const char *pass, const char *address,
-                        const uint8 *expected_host_id )
+                    const uint8 *expected_host_id )
 {
 	const char *(args[4]) = { name, pass, address, (const char *)expected_host_id };
-	printf( " Acquiring GVL for 'connect' event.\n" );
+	DEBUGMSG( " Acquiring GVL for 'connect' event.\n" );
 	fflush( stdout );
 	rb_thread_call_with_gvl( rbverse_cb_connect_body, args );
 }
 
 
 /*
- * call-seq:
- *    Verse.add_observer( observer )
- *
- * @see Verse::Observable#add_observer
- *
+ * Iterator for the node-index subscription callback.
  */
 static VALUE
-rbverse_verse_add_observer( VALUE module, VALUE observer ) {
+rbverse_cb_node_index_subscribe_i( VALUE observer, VALUE cb_args ) {
+	if ( !rb_obj_is_kind_of(observer, rbverse_mVerseConnectionObserver) )
+		return Qnil;
 
-	/* Register callbacks when an observer is added; I don't think re-registering
-	   them on observers after the first has any negative consequences...
-	 */
-	verse_callback_set( verse_send_ping, rbverse_cb_ping, NULL );
-	verse_callback_set( verse_send_connect, rbverse_cb_connect, NULL );
-
-	/* TODO: Add callbacks for:
-	 * 	     * verse_node_subscribe/unsubscribe
-	 */
-
-	return rb_call_super( 1, &observer );
+	rbverse_log( "debug", "Node-index subscription callback: notifying observer: %s.",
+	             RSTRING_PTR(rb_inspect( observer )) );
+	return rb_funcall2( observer, rb_intern("on_node_index_subscribe"),
+	                    RARRAY_LEN(cb_args), RARRAY_PTR(cb_args) );
 }
 
 
 /*
- * call-seq:
- *    Verse.remove_observer( observer )
- *
- * @see Verse::Observable#remove_observer
- *
+ * Call the 'node_index_subscribe' handler after acquiring the GVL.
  */
-static VALUE
-rbverse_verse_remove_observer( VALUE module, VALUE observer ) {
-	VALUE rval = Qnil;
+static void *
+rbverse_cb_node_index_subscribe_body( void *ptr ) {
+	const uint32 mask = *((uint32 *)ptr);
+	const VALUE observers = rb_iv_get( rbverse_mVerse, "@observers" );
+	const VALUE cb_args = rb_ary_new();
+	VALUE node_class = Qnil;
+	VNodeType node_type;
 
-	rval = rb_call_super( 1, &observer );
-
-	/* Unregister callbacks if the last observer has been removed */
-	if ( !RARRAY_LEN(rb_iv_get( rbverse_mVerse, "@observers" )) ) {
-		verse_callback_set( verse_send_ping, NULL, NULL );
-		verse_callback_set( verse_send_connect, NULL, NULL );
-
-		/* TODO: Remove callbacks for verse_node_subscribe/unsubscribe */
+	rbverse_log( "debug", "Building the list of subscribed classes from mask: %u.", mask );
+	for ( node_type = V_NT_OBJECT; node_type < V_NT_NUM_TYPES; node_type++ ) {
+		if ( mask & (1 << node_type) ) {
+			node_class = rbverse_node_class_from_node_type( node_type );
+			rbverse_log( "debug", "  adding %s", rb_class2name(node_class) );
+			rb_ary_push( cb_args, node_class );
+		}
 	}
 
-	return rval;
+	rbverse_log( "debug", "Calling node_index_subscribe iterator with %d node classes.",
+	             RARRAY_LEN(cb_args) );
+	rb_block_call( observers, rb_intern("each"), 0, 0, rbverse_cb_node_index_subscribe_i, cb_args );
+
+	return NULL;
 }
 
 
+/*
+ * Callback for the 'node_index_subscribe' command.
+ */
+static void
+rbverse_cb_node_index_subscribe( void *unused, uint32 mask ) {
+	rb_thread_call_with_gvl( rbverse_cb_node_index_subscribe_body, (void *)&mask );
+}
+
 
 /*
  * Verse namespace.
 
 	rbverse_mVerseVersioned = rb_define_module_under( rbverse_mVerse, "Versioned" );
 	rbverse_mVerseObserver = rb_define_module_under( rbverse_mVerse, "Observer" );
+	rbverse_mVerseObservable = rb_define_module_under( rbverse_mVerse, "Observable" );
+
 	rbverse_mVersePingObserver = rb_define_module_under( rbverse_mVerse, "PingObserver" );
 	rbverse_mVerseConnectionObserver = rb_define_module_under( rbverse_mVerse, "ConnectionObserver" );
-	rbverse_mVerseSessionObserver = rb_define_module_under( rbverse_mVerse, "SessionObserver" );
-	rbverse_mVerseObservable = rb_define_module_under( rbverse_mVerse, "Observable" );
 
 	rbverse_eVerseError =
 		rb_define_class_under( rbverse_mVerse, "Error", rb_eRuntimeError );
 	rb_define_singleton_method( rbverse_mVerse, "connect_accept", rbverse_verse_connect_accept, 3 );
 	rb_define_singleton_method( rbverse_mVerse, "ping", rbverse_verse_ping, 2 );
 
-	rb_define_singleton_method( rbverse_mVerse, "add_observer", rbverse_verse_add_observer, 1 );
-	rb_define_singleton_method( rbverse_mVerse, "remove_observer", rbverse_verse_remove_observer, 1 );
-
 	/*
 	 * Constants
 	 */
 	/* Init the subordinate classes */
 	rbverse_init_verse_session();
 	rbverse_init_verse_node();
+	rbverse_init_verse_mixins();
 
-	rbverse_init_verse_mixins();
+	/* Set up calbacks */
+	verse_callback_set( verse_send_ping, rbverse_cb_ping, NULL );
+	verse_callback_set( verse_send_connect, rbverse_cb_connect, NULL );
+	verse_callback_set( verse_send_node_index_subscribe, rbverse_cb_node_index_subscribe, NULL );
 
 	rbverse_log( "debug", "Initialized the extension." );
 }
 #include <inttypes.h>
 #include <math.h>
 
-#include <verse.h>
+#include "verse.h"
 
 #include "ruby.h"
 #ifndef RUBY_VM
 #	error Ruby-Verse requires at least Ruby 1.9.1
 #else
-#	include "ruby/intern.h"
 #	include "ruby/encoding.h"
 #	include "ruby/st.h"
 #endif /* !RUBY_VM */
 
+#ifdef DEBUG
+#	define DEBUGMSG(format, args...) fprintf( stderr, "\033[31m"format"\033[0m", ##args );
+#else
+#	define DEBUGMSG(format, args...)
+#endif
+
 /* Missing declarations of "experimental" thread functions from ruby/thread.c */
 void * rb_thread_call_with_gvl(void *(*)(void *), void *);
 
 extern VALUE rbverse_mVersePingObserver;
 extern VALUE rbverse_mVerseConnectionObserver;
 extern VALUE rbverse_mVerseSessionObserver;
+extern VALUE rbverse_mVerseNodeObserver;
 
+extern VALUE rbverse_cVerseServer;
 extern VALUE rbverse_cVerseSession;
 extern VALUE rbverse_cVerseNode;
 
  * Typedefs
  * -------------------------------------------------------------- */
 
-/* Struct for carrying parameters across an rb_thread_call_with_gvl() */
-typedef struct rbverse_connect_accept_event {
-	VNodeID		    avatar;
-	const char	    *address;
-	uint8		    *hostid;
-} rbverse_CONNECT_ACCEPT_EVENT;
+/* Class structures */
+struct rbverse_session {
+	VSession id;
+	VALUE    address;
+	VALUE    create_callbacks;
+	VALUE    destroy_callbacks;
+};
 
-typedef struct rbverse_node_create_event {
-	VNodeID node_id;
-	VNodeType type;
-	VNodeOwner owner;
-} rbverse_NODE_CREATE_EVENT;
-
-/* Class structures */
-typedef struct rbverse_session {
-	VSession id;
-	VALUE    self;
-	VALUE    address;
-} rbverse_SESSION;
-
-typedef struct rbverse_node {
+struct rbverse_node {
 	VNodeID		id;
 	VNodeType	type;
 	VNodeOwner	owner;
 			VALUE streams;
 		} audio;
 	};
-} rbverse_NODE;
+};
 
 
 /* Verse::Node globals. These are used to hook up child classes into
  * Verse::Node's memory-management and node-creation functions. */
 extern VALUE rbverse_nodetype_to_nodeclass[];
-extern void ( *node_mark_funcs[] )(rbverse_NODE *);
-extern void ( *node_free_funcs[] )(rbverse_NODE *);
+extern void ( *node_mark_funcs[] )(struct rbverse_node *);
+extern void ( *node_free_funcs[] )(struct rbverse_node *);
 
 
 
  * Macros
  * -------------------------------------------------------------- */
 #define IsSession( obj ) rb_obj_is_kind_of( (obj), rbverse_cVerseSession )
+#define IsServer( obj ) rb_obj_is_kind_of( (obj), rbverse_cVerseServer )
 
 #define IsNode( obj ) rb_obj_is_kind_of( (obj), rbverse_cVerseNode )
 #define IsAudioNode( obj ) rb_obj_is_kind_of( (obj), rbverse_cVerseAudioNode )
 extern VALUE rbverse_verse_session_from_vsession	_(( VSession, VALUE ));
 
 /* node.c */
+extern VALUE rbverse_node_class_from_node_type		_(( VNodeType  ));
 extern VALUE rbverse_wrap_verse_node				_(( VNodeID, VNodeType, VNodeOwner ));
 extern VALUE rbverse_lookup_verse_node				_(( VNodeID ));
 extern void rbverse_mark_node_destroyed				_(( VALUE ));
-extern rbverse_NODE * rbverse_get_node				_(( VALUE ));
+extern struct rbverse_node * rbverse_get_node				_(( VALUE ));
 
 
 /* --------------------------------------------------------------
 
 void Init_verse_ext( void );
 
+extern void rbverse_init_verse_server       _(( void ));
 extern void rbverse_init_verse_session      _(( void ));
 extern void rbverse_init_verse_mixins       _(( void ));
 

lib/verse/mixins.rb

 
 
 		### Remove all current observers.
-		### @returns [Array<Verse::Observer>]  the observers that were removed
+		### @return [Array<Verse::Observer>]  the observers that were removed
 		def remove_observers
 			@observers.collect {|observer| self.remove_observer(observer) }
 		end
 		### Remove the receiver from the list of interested observers from
 		### the given +objects+.
 		### 
-		### @params [Array<Observable>] objects the objects to no longer observe.
+		### @param [Array<Observable>] objects the objects to no longer observe.
 		def stop_observing( *objects )
 			objects.each {|obj| obj.remove_observer(self) }
 		end
 			self.log.debug "unhandled on_connect: '%s' connected from '%s'" % [ user, address ]
 		end
 
+
+		### Called when a Verse client requests subscriptions to create/destroy events
+		### for the specified node +classes+.
+		### 
+		### @param [Array<Class>] classes  the Verse::Node classes to subscribe to.
+		def on_node_index_subscribe( *classes )
+			classes.flatten!
+			if classes.empty?
+				self.log.debug "unhandled node_index_subscribe: clear subscriptions"
+			else
+				self.log.debug "unhandled node_index_subscribe: classes: %s" %
+					[ classes.collect {|kl| kl.name }.join(', ') ]
+			end
+		end
+
 	end # module ConnectionObserver
 
 
 		### @param [String]  address  the address of the terminating client
 		### @param [String]  message  the termination message
 		def on_connect_terminate( address, message )
-			self.log.debug "unhandled on_connect_terminate: '%s' terminated communication: %s" % 
+			self.log.debug "unhandled on_connect_terminate: '%s' terminated communication: %s" %
 				[ address, message ]
 		end
 
 	end # module SessionObserver
 
 
+	### A mixin for objects which wish to observe events on a Verse::Node.
+	module NodeObserver
+		include Verse::Loggable,
+		        Verse::Observer
+
+		### Called when the verse server sets the node's name.
+		### 
+		### @param [Verse::Node] node  the node that has just had its name set/changed.
+		### @param [String] name       the node's new name.
+		def on_node_name_set( node, name )
+			self.log.debug "unhandled on_node_name_set: %p is now named %p" % [ node, name ]
+		end
+
+		### Called when a new +tag_group+ is created for the +node+.
+		### 
+		### @param [Verse::Node] node                 the node the tag group belongs to
+		### @param [Verse::Node::TagGroup] tag_group  the new tag group
+		def on_tag_group_create( node, tag_group )
+			self.log.debug "unhandled on_tag_group_create: node %p now has tag group %p" %
+			 	[ node, tag_group ]
+		end
+
+		### Called when a +tag_group+ is destroyed.
+		### 
+		### @param [Verse::Node] node                 the node the tag group used to belong to.
+		### @param [Verse::Node::TagGroup] tag_group  the tag group that has been destroyed.
+		def on_tag_group_destroy( node, tag_group )
+			self.log.debug "unhandled on_tag_group_destroy: node %p no longer has tag group %p" %
+			 	[ node, tag_group ]
+		end
+
+		### Called when the verse server sets the node's name.
+		### 
+		### @param [Verse::Node] node  the node that has just had its name set/changed.
+		### @param [String] name       the node's new name.
+		def on_tag_group_subscribe( node )
+			self.log.debug "unhandled on_node_destroy for %p" % [ node ]
+		end
+
+		### Called when the verse server sets the node's name.
+		### 
+		### @param [Verse::Node] node  the node that has just had its name set/changed.
+		### @param [String] name       the node's new name.
+		def on_tag_group_unsubscribe( node )
+			self.log.debug "unhandled on_node_destroy for %p" % [ node ]
+		end
+
+		### Called when the verse server sets the node's name.
+		### 
+		### @param [Verse::Node] node  the node that has just had its name set/changed.