Browse Source

Initial Commit

Keelan Lightfoot 8 years ago
commit
47a62445d3
7 changed files with 1658 additions and 0 deletions
  1. 24
    0
      LICENSE
  2. 73
    0
      README.md
  3. 821
    0
      client.go
  4. 123
    0
      cmd/funmow/main.go
  5. 114
    0
      event_distributor.go
  6. 195
    0
      object.go
  7. 308
    0
      world.go

+ 24
- 0
LICENSE View File

@@ -0,0 +1,24 @@
1
+This is free and unencumbered software released into the public domain.
2
+
3
+Anyone is free to copy, modify, publish, use, compile, sell, or
4
+distribute this software, either in source code form or as a compiled
5
+binary, for any purpose, commercial or non-commercial, and by any
6
+means.
7
+
8
+In jurisdictions that recognize copyright laws, the author or authors
9
+of this software dedicate any and all copyright interest in the
10
+software to the public domain. We make this dedication for the benefit
11
+of the public at large and to the detriment of our heirs and
12
+successors. We intend this dedication to be an overt act of
13
+relinquishment in perpetuity of all present and future rights to this
14
+software under copyright law.
15
+
16
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
19
+IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
20
+OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
21
+ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
22
+OTHER DEALINGS IN THE SOFTWARE.
23
+
24
+For more information, please refer to <http://unlicense.org>

+ 73
- 0
README.md View File

@@ -0,0 +1,73 @@
1
+# FunMOW
2
+A multiplayer text based game written in Go, with MU* inspired userspace. Being MU* inspired, FunMOW has support for expanding the world with a set of in-game commands.
3
+
4
+## Installing
5
+
6
+	go get github.com/naleek/funmow/...
7
+	
8
+## Running
9
+	funmow
10
+
11
+## Connecting
12
+To fully enjoy FunMOW, use a MU* client. In testing (MacOS), the bog standard `telnet` command doesn't support unicode, but netcat (`nc`) does.
13
+
14
+## Issues
15
+
16
+* No configuration options with modifying source
17
+* Authentication and encryption are non-existent
18
+* Database access is abusive, in anticipation of switching to an in-memory based database, with disk persistence
19
+* Failure modes are ~~often~~ not reported to the player
20
+
21
+
22
+## Commands
23
+
24
+### Documentation Conventions
25
+`{command,alternate}`
26
+
27
+`[optional parameter]`
28
+
29
+`<required parameter>`
30
+
31
+### Database References
32
+All objects in the game have a database reference (dbref), which is a signed integer assigned when the object is created. Dbrefs are incremented sequentially.
33
+
34
+### Object References
35
+
36
+Object references support partial matches, if you see a frog and type `get f`, you will acquire the frog. An object can be referred to by dbref by using the number preceeded by am octothorpe. This is handy when you end up with two objects with the same name, and only want to get one of them: `get #123`
37
+
38
+## Login Commands
39
+These commands are only valid in the authentication phase of gameplay.
40
+
41
+	{con,connect} <username> <password>
42
+	quit
43
+
44
+## Basic Commands
45
+
46
+This is the stuff that you use the most when you're not building things.
47
+
48
+	{look,l} [object reference]
49
+	{examine,ex} <object reference>
50
+	{say,"} <something>
51
+	{pose,:} <action>
52
+	{inventory,i}
53
+	get <object reference>
54
+	drop <object reference>
55
+	enter <object reference>
56
+	leave
57
+	quit
58
+	
59
+
60
+### Building Commands
61
+
62
+A note on exits. Exits support fixed aliases, so that you have the freedom to assign hidden shortcuts to exits. For example, `<K>itchen;k;kitchen` will display as `<K>itchen`, but can be used as `<K>itchen` or `k` or `kitchen`. You can use as many aliases as you want on an exit.
63
+
64
+	@create <name>
65
+	@dig <name>[=<in exit>[,<out exit]]
66
+	@open <in exit>=<dbref>[,<out exit>]
67
+	@desc <object reference>=<description>
68
+	@name <object reference>=<name>
69
+	@tel <dbref>
70
+	@dump <object reference>
71
+	
72
+# Credits
73
+FunMOW was created and is maintained by Keelan Lightfoot.

+ 821
- 0
client.go View File

@@ -0,0 +1,821 @@
1
+package funmow
2
+
3
+import (
4
+	"bufio"
5
+	"fmt"
6
+	"log"
7
+	"net"
8
+	"strings"
9
+	"unicode"
10
+)
11
+
12
+type Client struct {
13
+	conn             net.Conn
14
+	connectionID     int
15
+	player           Object
16
+	authenticated    bool
17
+	connected        bool
18
+	reader           *bufio.Reader
19
+	commandChan      chan string
20
+	inbound          chan PlayerEvent
21
+	outbound         EventChanSet
22
+	eventDistributor *EventDistributor
23
+	world            *Store
24
+}
25
+
26
+func NewClient(conn net.Conn, connectionID int, e *EventDistributor, w *Store) *Client {
27
+	c := new(Client)
28
+	c.conn = conn
29
+	c.connectionID = connectionID
30
+	c.reader = bufio.NewReader(conn)
31
+	c.authenticated = false
32
+	c.connected = true
33
+	c.commandChan = make(chan string)
34
+	c.inbound = make(chan PlayerEvent)
35
+	c.eventDistributor = e
36
+	c.world = w
37
+	return c
38
+}
39
+
40
+func (c *Client) Run() {
41
+
42
+	log.Print("Received connection from ", c.conn.RemoteAddr())
43
+
44
+	go func() {
45
+		for c.connected {
46
+			message, err := c.reader.ReadString('\n')
47
+			if err != nil {
48
+				c.connected = false
49
+				return
50
+			}
51
+			message = strings.TrimSpace(message)
52
+			if len(message) > 0 {
53
+				c.commandChan <- message
54
+			}
55
+		}
56
+	}()
57
+
58
+	c.splash()
59
+
60
+	for c.connected {
61
+		select {
62
+		case m := <-c.inbound:
63
+
64
+			switch m.messageType {
65
+			case EventTypeEmit:
66
+				fallthrough
67
+			case EventTypeOEmit:
68
+				if m.audience == c.player.Inside {
69
+					c.write("%s\n", m.message)
70
+				}
71
+			case EventTypePEmit:
72
+				c.write("%s\n", m.message)
73
+			case EventTypeWall:
74
+				speaker, found := c.world.Fetch(m.src)
75
+				if !found {
76
+					break
77
+				}
78
+				c.write("In the distance, you hear %s bellow out \"%s\"\n", speaker.Name, m.message)
79
+			case EventTypePage:
80
+				speaker, found := c.world.Fetch(m.src)
81
+				if !found {
82
+					break
83
+				}
84
+				c.write("%s pages you: \"%s\"\n", speaker.Name, m.message)
85
+			case EventTypeSay:
86
+				if m.audience == c.player.Inside {
87
+					if m.src == c.player.ID {
88
+						c.write("You say \"%s\"\n", m.message)
89
+					} else {
90
+						speaker, found := c.world.Fetch(m.src)
91
+						if !found {
92
+							break
93
+						}
94
+						c.write("%s says \"%s\"\n", speaker.Name, m.message)
95
+					}
96
+				}
97
+			case EventTypePose:
98
+				if m.audience == c.player.Inside {
99
+					if m.src == c.player.ID {
100
+						c.write("%s %s\n", c.player.Name, m.message)
101
+					} else {
102
+						speaker, found := c.world.Fetch(m.src)
103
+						if !found {
104
+							break
105
+						}
106
+						c.write("%s %s\n", speaker.Name, m.message)
107
+					}
108
+				}
109
+			}
110
+		case cmd := <-c.commandChan:
111
+			if c.authenticated {
112
+				c.handlePlayPhase(cmd)
113
+			} else {
114
+				c.handleLoginPhase(cmd)
115
+			}
116
+		}
117
+	}
118
+	if c.authenticated {
119
+		c.outbound.shutdownChan <- c.connectionID
120
+	}
121
+	c.conn.Close()
122
+
123
+	log.Print("Lost connection from ", c.conn.RemoteAddr())
124
+
125
+}
126
+
127
+func (c *Client) splash() {
128
+
129
+	c.write("Welcome to...\n")
130
+	c.write("  ___                __  __    ___  __      __\n")
131
+	c.write(" | __| _  _   _ _   |  \\/  |  / _ \\ \\ \\    / /\n")
132
+	c.write(" | _| | || | | ' \\  | |\\/| | | (_) | \\ \\/\\/ /\n")
133
+	c.write(" |_|   \\_,_| |_||_| |_|  |_|  \\___/   \\_/\\_/\n\n")
134
+	c.write("use connect <username> <password> to login.\n")
135
+
136
+}
137
+
138
+func (c *Client) handleLoginPhase(message string) {
139
+
140
+	fields := strings.FieldsFunc(message, func(c rune) bool {
141
+		return unicode.IsSpace(c)
142
+	})
143
+	switch fields[0] {
144
+	case "help":
145
+		c.write("Commands:\n\tcon[nect] <username>\n\tquit\n")
146
+	case "con":
147
+		fallthrough
148
+	case "connect":
149
+		if len(fields) == 3 {
150
+			pID, err := c.world.GetPlayerID(fields[1])
151
+			if err != nil {
152
+				c.write("Bad username or password.\n")
153
+				break
154
+			}
155
+			player, found := c.world.Fetch(pID)
156
+			if !found {
157
+				c.write("You appear to be having an existential crisis.\n")
158
+				c.connected = false
159
+				break
160
+			}
161
+
162
+			c.authenticated = true
163
+			c.player = player
164
+
165
+			// when a player authenticates, the ipc channels get turned on
166
+			subReq := EventSubscribeRequest{
167
+				connectionID: c.connectionID,
168
+				playerID:     c.player.ID,
169
+				inbound:      c.inbound,
170
+				chanSet:      make(chan EventChanSet),
171
+			}
172
+
173
+			c.outbound = c.eventDistributor.Subscribe(subReq)
174
+
175
+			c.write("Welcome back, %s!\n", fields[1])
176
+
177
+			c.lookCmd("")
178
+
179
+		} else {
180
+			c.write("What?\n")
181
+		}
182
+	case "quit":
183
+		c.quitCmd()
184
+	default:
185
+		c.write("What?\n")
186
+	}
187
+
188
+}
189
+
190
+func (c *Client) handlePlayPhase(message string) {
191
+
192
+	switch {
193
+	case message == "l":
194
+		c.lookCmd("")
195
+	case message == "look":
196
+		c.lookCmd("")
197
+	case strings.HasPrefix(message, "l "): // look at
198
+		c.lookCmd(strings.TrimPrefix(message, "l "))
199
+	case strings.HasPrefix(message, "look "): // look at
200
+		c.lookCmd(strings.TrimPrefix(message, "look "))
201
+	case strings.HasPrefix(message, "ex "):
202
+		c.examineCmd(strings.TrimPrefix(message, "ex "))
203
+	case strings.HasPrefix(message, "examine "):
204
+		c.examineCmd(strings.TrimPrefix(message, "examine "))
205
+	case strings.HasPrefix(message, "\""):
206
+		c.sayCmd(strings.TrimPrefix(message, "\""))
207
+	case strings.HasPrefix(message, "say "):
208
+		c.sayCmd(strings.TrimPrefix(message, "say "))
209
+	case strings.HasPrefix(message, ":"):
210
+		c.poseCmd(strings.TrimPrefix(message, ":"))
211
+	case strings.HasPrefix(message, "pose "):
212
+		c.poseCmd(strings.TrimPrefix(message, "pose "))
213
+	case message == "i":
214
+		c.inventoryCmd()
215
+	case message == "inventory":
216
+		c.inventoryCmd()
217
+	case strings.HasPrefix(message, "get "):
218
+		c.getCmd(strings.TrimPrefix(message, "get "))
219
+	case strings.HasPrefix(message, "drop "):
220
+		c.dropCmd(strings.TrimPrefix(message, "drop "))
221
+	case strings.HasPrefix(message, "enter "):
222
+		c.enterCmd(strings.TrimPrefix(message, "enter "))
223
+	case message == "leave":
224
+		c.leaveCmd()
225
+	case message == "quit":
226
+		c.quitCmd()
227
+	case message == "WHO":
228
+		c.whoCmd()
229
+	case strings.HasPrefix(message, "@create "):
230
+		c.createCmd(strings.TrimPrefix(message, "@create "))
231
+	case strings.HasPrefix(message, "@dig "):
232
+		c.digCmd(strings.TrimPrefix(message, "@dig "))
233
+	case strings.HasPrefix(message, "@open "):
234
+		c.openCmd(strings.TrimPrefix(message, "@open "))
235
+	case strings.HasPrefix(message, "@name "):
236
+		c.nameCmd(strings.TrimPrefix(message, "@name "))
237
+	case strings.HasPrefix(message, "@desc "):
238
+		c.descCmd(strings.TrimPrefix(message, "@desc "))
239
+	case strings.HasPrefix(message, "@tel "):
240
+		c.telCmd(strings.TrimPrefix(message, "@tel "))
241
+	case strings.HasPrefix(message, "@dump "):
242
+		c.dumpCmd(strings.TrimPrefix(message, "@dump "))
243
+	default:
244
+		if !c.goCmd(message) {
245
+			c.write("What?\n")
246
+		}
247
+	}
248
+
249
+}
250
+
251
+func (c *Client) lookCmd(at string) {
252
+
253
+	room, found := c.world.Fetch(c.player.Inside)
254
+	if !found {
255
+		c.write("Limbo\nThere's nothing to see here.\n")
256
+		return
257
+	}
258
+
259
+	if len(at) > 0 {
260
+		object, matchType := room.MatchLinkNames(at, c.player.ID, false).ExactlyOne()
261
+		switch matchType {
262
+		case MatchOne:
263
+			c.write("%s\n%s\n", object.DetailedName(), object.Description)
264
+		case MatchNone:
265
+			c.write("I don't see that here.\n")
266
+		case MatchMany:
267
+			c.write("I don't now which one you're trying to look at.\n")
268
+		}
269
+	} else {
270
+		c.write("%s\n%s\n", room.DetailedName(), room.Description)
271
+		c.lookLinks("thing", "Things:")
272
+		c.lookLinks("player", "Players:")
273
+		exits := room.GetLinkNames("exit", nil)
274
+		if len(exits) > 0 {
275
+			c.write("Exits:\n")
276
+			for _, e := range exits {
277
+				aliases := strings.Split(e, ";")
278
+				c.write("%s ", aliases[0])
279
+			}
280
+			c.write("\n")
281
+		}
282
+	}
283
+
284
+}
285
+
286
+func (c *Client) examineCmd(at string) {
287
+
288
+	room, found := c.world.Fetch(c.player.Inside)
289
+	if !found {
290
+		return
291
+	}
292
+
293
+	object, matchType := room.MatchLinkNames(at, c.player.ID, false).ExactlyOne()
294
+
295
+	switch matchType {
296
+	case MatchOne:
297
+		c.write("%s\n", object.DetailedName())
298
+		c.write("ID: %d\n", object.ID)
299
+		c.write("Type: %s\n", object.Type)
300
+		c.write("@name: %s\n", object.Name)
301
+		c.write("@desc: %s\n", object.Description)
302
+		c.write("Inside: %s\n", c.world.GetName(object.Inside))
303
+		c.write("Next: %s\n", c.world.GetName(object.Next))
304
+		c.write("Owner: %s\n", c.world.GetName(object.Owner))
305
+
306
+		inventory := object.GetLinkNames("*", nil)
307
+		if len(inventory) > 0 {
308
+			c.write("Contents:\n %s\n", strings.Join(inventory, "\n "))
309
+		}
310
+	case MatchNone:
311
+		c.write("I don't see that here.\n")
312
+	case MatchMany:
313
+		c.write("I don't know which one you're trying to examine.\n")
314
+	}
315
+
316
+}
317
+
318
+func (c *Client) lookLinks(linkType string, pretty string) {
319
+
320
+	room, found := c.world.Fetch(c.player.Inside)
321
+	if !found {
322
+		return
323
+	}
324
+
325
+	linknames := room.GetLinkNames(linkType, DBRefList{c.player.ID})
326
+
327
+	if len(linknames) > 0 {
328
+		c.write("%s\n %s\n", pretty, strings.Join(linknames, "\n "))
329
+	}
330
+
331
+}
332
+
333
+func (c *Client) sayCmd(message string) {
334
+
335
+	c.outbound.messageChan <- PlayerEvent{audience: c.player.Inside, src: c.player.ID, message: message, messageType: EventTypeSay}
336
+
337
+}
338
+
339
+func (c *Client) poseCmd(message string) {
340
+
341
+	c.outbound.messageChan <- PlayerEvent{audience: c.player.Inside, src: c.player.ID, dst: c.player.ID, message: message, messageType: EventTypePose}
342
+
343
+}
344
+
345
+func (c *Client) inventoryCmd() {
346
+
347
+	inventory := c.player.GetLinkNames("*", nil)
348
+
349
+	if len(inventory) > 0 {
350
+		c.write("Inventory:\n %s\n", strings.Join(inventory, "\n "))
351
+	} else {
352
+		c.write("You're not carrying anything.\n")
353
+	}
354
+
355
+}
356
+
357
+func (c *Client) getCmd(message string) {
358
+
359
+	room, found := c.world.Fetch(c.player.Inside)
360
+	if !found {
361
+		return
362
+	}
363
+
364
+	object, matchType := room.MatchLinkNames(message, c.player.ID, true).ExactlyOne()
365
+
366
+	switch matchType {
367
+	case MatchOne:
368
+		err := c.player.Contains(&object)
369
+		if err != nil {
370
+			return
371
+		}
372
+		c.player.Refresh()
373
+		c.write("You pick up %s.\n", object.Name)
374
+		c.oemit(room.ID, "%s picks up %s.", c.player.Name, object.Name)
375
+	case MatchNone:
376
+		c.write("I don't see that here.\n")
377
+	case MatchMany:
378
+		c.write("I don't know which one.\n")
379
+	}
380
+
381
+}
382
+
383
+func (c *Client) dropCmd(message string) {
384
+
385
+	room, found := c.world.Fetch(c.player.Inside)
386
+	if !found {
387
+		return
388
+	}
389
+
390
+	object, matchType := c.player.MatchLinkNames(message, c.player.ID, false).ExactlyOne()
391
+
392
+	switch matchType {
393
+	case MatchOne:
394
+		err := room.Contains(&object)
395
+		if err != nil {
396
+			return
397
+		}
398
+		c.player.Refresh()
399
+		c.write("You drop %s.\n", object.Name)
400
+		c.oemit(c.player.Inside, "%s drops %s.", c.player.Name, object.Name)
401
+	case MatchNone:
402
+		c.write("You're not carrying that.\n")
403
+	case MatchMany:
404
+		c.write("I don't now which one.\n")
405
+	}
406
+
407
+}
408
+
409
+func (c *Client) enterCmd(message string) {
410
+
411
+	room, found := c.world.Fetch(c.player.Inside)
412
+	if !found {
413
+		return
414
+	}
415
+
416
+	object, matchType := room.MatchLinkNames(message, c.player.ID, true).ExactlyOne()
417
+
418
+	switch matchType {
419
+	case MatchOne:
420
+		err := object.Contains(&c.player)
421
+		if err != nil {
422
+			return
423
+		}
424
+		c.player.Refresh()
425
+		c.write("You climb into %s.\n", object.Name)
426
+		c.oemit(room.ID, "%s climbs into %s.", c.player.Name, object.Name)
427
+		c.oemit(object.ID, "%s squeezes into %s with you.", c.player.Name, object.Name)
428
+	case MatchNone:
429
+		c.write("I don't see that here.\n")
430
+	case MatchMany:
431
+		c.write("I don't now which one.\n")
432
+	}
433
+}
434
+
435
+func (c *Client) leaveCmd() {
436
+
437
+	object, found := c.world.Fetch(c.player.Inside)
438
+	if !found {
439
+		return
440
+	}
441
+
442
+	if object.Inside == 0 { // probably trying to 'leave' a room
443
+		c.write("You can't leave here.\n")
444
+		return
445
+	}
446
+
447
+	room, found := c.world.Fetch(object.Inside)
448
+	if !found {
449
+		return
450
+	}
451
+
452
+	err := room.Contains(&c.player)
453
+	if err != nil {
454
+		return
455
+	}
456
+
457
+	c.player.Refresh()
458
+
459
+	c.write("You climb out of %s.\n", object.Name)
460
+	c.oemit(object.ID, "%s climbs out of %s.", c.player.Name, object.Name)
461
+	c.oemit(room.ID, "%s climbs out of %s.", c.player.Name, object.Name)
462
+
463
+}
464
+
465
+func (c *Client) quitCmd() {
466
+
467
+	c.write("So long, it's been good to know yah.\n")
468
+	c.connected = false
469
+
470
+}
471
+
472
+func (c *Client) whoCmd() {
473
+	onlinePlayers := c.eventDistributor.OnlinePlayers()
474
+
475
+	c.write("Currently Online:\n")
476
+
477
+	for _, ref := range onlinePlayers {
478
+		c.write("%s\n", c.world.GetName(ref))
479
+	}
480
+}
481
+
482
+func (c *Client) createCmd(message string) {
483
+
484
+	room, found := c.world.Fetch(c.player.Inside)
485
+	if !found {
486
+		return
487
+	}
488
+
489
+	o, _ := c.world.Allocate("thing")
490
+	o.Name = strings.TrimSpace(message)
491
+	o.Owner = c.player.ID
492
+	o.Commit()
493
+
494
+	err := room.Contains(&o)
495
+	if err != nil {
496
+		return
497
+	}
498
+
499
+	c.emit(room.ID, "A %s appears out of the ether.", o.Name)
500
+	c.write("%s Created.\n", o.DetailedName())
501
+
502
+}
503
+
504
+func (c *Client) openCmd(message string) {
505
+	// @open <in1;in2;in3;etc>=#<room>,<out1;out2;out3;etc>
506
+
507
+	slicey := strings.SplitN(message, "=", 2)
508
+
509
+	if len(slicey) < 2 {
510
+		c.write("Bad command or file name.\n")
511
+		return
512
+	}
513
+
514
+	srcExitSpec := strings.TrimSpace(slicey[0])
515
+
516
+	stuff := strings.Split(slicey[1], ",")
517
+	if len(stuff) > 2 {
518
+		c.write("Bad command or file name.\n")
519
+		return
520
+	}
521
+
522
+	makeReturnExit := false
523
+	var returnExitSpec string
524
+	if len(stuff) == 2 {
525
+		makeReturnExit = true
526
+		returnExitSpec = stuff[1]
527
+	}
528
+
529
+	target, err := NewDBRefFromHashRef(stuff[0])
530
+	if err != nil {
531
+		c.write("Bad target DBRef.\n")
532
+		return
533
+	}
534
+
535
+	room, found := c.world.Fetch(c.player.Inside)
536
+	if !found {
537
+		return
538
+	}
539
+
540
+	targetRoom, found := c.world.Fetch(target)
541
+	if !found {
542
+		c.write("Target not found.\n")
543
+		return
544
+	}
545
+
546
+	toExit, _ := c.world.Allocate("exit")
547
+	toExit.Name = srcExitSpec
548
+	toExit.Next = targetRoom.ID
549
+	toExit.Owner = c.player.ID
550
+	toExit.Commit()
551
+
552
+	err = room.Contains(&toExit)
553
+	if err != nil {
554
+		return
555
+	}
556
+
557
+	c.write("%s Created.\n", toExit.DetailedName())
558
+
559
+	if makeReturnExit {
560
+		fromExit, _ := c.world.Allocate("exit")
561
+		fromExit.Name = returnExitSpec
562
+		fromExit.Next = room.ID
563
+		fromExit.Owner = c.player.ID
564
+		fromExit.Commit()
565
+
566
+		err = targetRoom.Contains(&fromExit)
567
+		if err != nil {
568
+			return
569
+		}
570
+
571
+		c.write("%s Created.\n", fromExit.DetailedName())
572
+	}
573
+
574
+}
575
+
576
+func (c *Client) digCmd(message string) {
577
+	// @dig <Room name>=<in1;in2;in3;etc>,<out1;out2;out3;etc>
578
+	//@dig foo=<F>oo;foo;f,<B>ack;back;b
579
+	slicey := strings.SplitN(message, "=", 2)
580
+
581
+	if len(slicey) < 1 {
582
+		c.write("Rooms can't not have names.\n")
583
+		return
584
+	}
585
+
586
+	roomName := strings.TrimSpace(slicey[0])
587
+
588
+	exits := make([]string, 0)
589
+
590
+	if len(slicey) == 2 {
591
+		exitSpec := strings.TrimSpace(slicey[1])
592
+		exits = strings.Split(exitSpec, ",")
593
+		if len(exits) > 2 {
594
+			c.write("You've listed more than two exits. That doesn't even make sense.\n")
595
+			return
596
+		}
597
+	}
598
+
599
+	newRoom, _ := c.world.Allocate("room")
600
+	newRoom.Name = roomName
601
+	newRoom.Owner = c.player.ID
602
+	newRoom.Commit()
603
+
604
+	c.write("%s Created.\n", newRoom.DetailedName())
605
+
606
+	if len(exits) > 0 {
607
+		room, found := c.world.Fetch(c.player.Inside)
608
+		if !found {
609
+			return
610
+		}
611
+		toExit, _ := c.world.Allocate("exit")
612
+		toExit.Name = exits[0]
613
+		toExit.Next = newRoom.ID
614
+		toExit.Owner = c.player.ID
615
+		toExit.Commit()
616
+
617
+		err := room.Contains(&toExit)
618
+		if err != nil {
619
+			return
620
+		}
621
+
622
+		c.write("%s Created.\n", toExit.DetailedName())
623
+
624
+		if len(exits) == 2 {
625
+			fromExit, _ := c.world.Allocate("exit")
626
+			fromExit.Name = exits[1]
627
+			fromExit.Next = room.ID
628
+			fromExit.Owner = c.player.ID
629
+			fromExit.Commit()
630
+
631
+			err = newRoom.Contains(&fromExit)
632
+			if err != nil {
633
+				return
634
+			}
635
+
636
+			c.write("%s Created.\n", fromExit.DetailedName())
637
+
638
+		}
639
+	}
640
+
641
+}
642
+
643
+func (c *Client) nameCmd(message string) {
644
+	room, found := c.world.Fetch(c.player.Inside)
645
+	if !found {
646
+		return
647
+	}
648
+
649
+	slicey := strings.SplitN(message, "=", 2)
650
+
651
+	if len(slicey) == 1 {
652
+		c.write("Things can't not have names.\n")
653
+		return
654
+	}
655
+
656
+	objectName := strings.TrimSpace(slicey[0])
657
+	name := strings.TrimSpace(slicey[1])
658
+
659
+	candidate, matchType := room.MatchLinkNames(objectName, c.player.ID, false).ExactlyOne()
660
+	switch matchType {
661
+	case MatchOne:
662
+		candidate.Name = name
663
+		candidate.Commit()
664
+		c.write("Name set.\n")
665
+		c.player.Refresh()
666
+	case MatchNone:
667
+		c.write("I don't see that here.\n")
668
+	case MatchMany:
669
+		c.write("I don't now which one.\n")
670
+	}
671
+
672
+}
673
+func (c *Client) descCmd(message string) {
674
+
675
+	room, found := c.world.Fetch(c.player.Inside)
676
+	if !found {
677
+		return
678
+	}
679
+
680
+	slicey := strings.SplitN(message, "=", 2)
681
+
682
+	objectName := strings.TrimSpace(slicey[0])
683
+	description := ""
684
+	if len(slicey) > 1 {
685
+		description = strings.TrimSpace(slicey[1])
686
+	}
687
+
688
+	var editObject *Object
689
+	switch objectName {
690
+	case "here":
691
+		editObject = &room
692
+	case "me":
693
+		editObject = &c.player
694
+	default:
695
+		candidate, matchType := room.MatchLinkNames(objectName, c.player.ID, false).ExactlyOne()
696
+		switch matchType {
697
+		case MatchOne:
698
+			editObject = &candidate
699
+		case MatchNone:
700
+			c.write("I don't see that here.\n")
701
+			return
702
+		case MatchMany:
703
+			c.write("I don't now which one.\n")
704
+			return
705
+		}
706
+	}
707
+
708
+	editObject.Description = description
709
+	editObject.Commit()
710
+	c.player.Refresh()
711
+	c.write("Description set.\n")
712
+
713
+}
714
+
715
+func (c *Client) telCmd(destStr string) {
716
+
717
+	dest, err := NewDBRefFromHashRef(destStr)
718
+
719
+	if err != nil {
720
+		c.write("That doesn't look like a DBRef.\n")
721
+		return
722
+	}
723
+
724
+	newRoom, found := c.world.Fetch(dest)
725
+	if !found {
726
+		c.write("That doesn't exist.\n")
727
+		return
728
+	}
729
+
730
+	c.write("You feel an intense wooshing sensation.\n")
731
+	err = newRoom.Contains(&c.player)
732
+	if err != nil {
733
+		return
734
+	}
735
+
736
+	c.player.Refresh()
737
+	c.lookCmd("")
738
+
739
+}
740
+
741
+func (c *Client) dumpCmd(refStr string) {
742
+
743
+	ref, err := NewDBRefFromHashRef(refStr)
744
+
745
+	if err != nil {
746
+		c.write("That doesn't look like a DBRef.\n")
747
+		return
748
+	}
749
+
750
+	obj, found := c.world.Fetch(ref)
751
+	if !found {
752
+		c.write("That doesn't exist.\n")
753
+		return
754
+	}
755
+
756
+	c.write("%s\n", c.world.DumpObject(obj.ID))
757
+
758
+}
759
+
760
+func (c *Client) goCmd(dir string) bool {
761
+
762
+	room, found := c.world.Fetch(c.player.Inside)
763
+	if !found {
764
+		return false
765
+	}
766
+
767
+	exit, matchType := room.MatchExitNames(dir).ExactlyOne()
768
+	switch matchType {
769
+	case MatchOne:
770
+		if exit.Next.Valid() {
771
+			newRoom, found := c.world.Fetch(exit.Next)
772
+			if !found {
773
+				return false
774
+			}
775
+			err := newRoom.Contains(&c.player)
776
+			if err != nil {
777
+				return false
778
+			}
779
+			c.player.Refresh()
780
+			c.write("You head towards %s.\n", newRoom.Name)
781
+			c.oemit(room.ID, "%s leaves the room.", c.player.Name)
782
+			c.oemit(newRoom.ID, "%s enters the room.", c.player.Name)
783
+			return true
784
+		}
785
+	case MatchNone:
786
+		return false
787
+	case MatchMany:
788
+		c.write("Ambiguous exit names are ambiguous.\n")
789
+		return true
790
+	}
791
+
792
+	return false
793
+
794
+}
795
+
796
+func (c *Client) oemit(audience DBRef, format string, a ...interface{}) {
797
+
798
+	message := fmt.Sprintf(format, a...)
799
+	c.outbound.messageChan <- PlayerEvent{audience: audience, src: c.player.ID, dst: c.player.ID, message: message, messageType: EventTypeOEmit}
800
+
801
+}
802
+
803
+func (c *Client) write(format string, a ...interface{}) {
804
+
805
+	fmt.Fprintf(c.conn, format, a...)
806
+
807
+}
808
+
809
+func (c *Client) pemit(audience DBRef, format string, a ...interface{}) {
810
+
811
+	message := fmt.Sprintf(format, a...)
812
+	c.outbound.messageChan <- PlayerEvent{audience: audience, src: c.player.ID, dst: c.player.ID, message: message, messageType: EventTypePEmit}
813
+
814
+}
815
+
816
+func (c *Client) emit(audience DBRef, format string, a ...interface{}) {
817
+
818
+	message := fmt.Sprintf(format, a...)
819
+	c.outbound.messageChan <- PlayerEvent{audience: audience, src: c.player.ID, dst: c.player.ID, message: message, messageType: EventTypeEmit}
820
+
821
+}

+ 123
- 0
cmd/funmow/main.go View File

@@ -0,0 +1,123 @@
1
+package main
2
+
3
+import (
4
+	"github.com/naleek/funmow"
5
+	"log"
6
+	"net"
7
+)
8
+
9
+const (
10
+	FunMOWVersion = "0.1"
11
+	FunMOWDefaultDB = "my.db"
12
+	FunMOWListen = ":4201"
13
+)
14
+
15
+func main() {
16
+
17
+	log.Print("Starting FunMOW Version ", FunMOWVersion)
18
+
19
+	log.Print("Using database ", FunMOWDefaultDB)
20
+	world := funmow.NewDB(FunMOWDefaultDB)
21
+	world.Open()
22
+
23
+	defer world.Close()
24
+
25
+	//seedDB(world)
26
+
27
+	log.Print("Listening for connections on ", FunMOWListen)
28
+	ln, err := net.Listen("tcp", FunMOWListen)
29
+	if err != nil {
30
+		log.Fatal(err)
31
+	}
32
+
33
+	log.Print("Starting event distributor...")
34
+
35
+	e := funmow.NewEventDistributor()
36
+	go e.Run()
37
+
38
+	log.Print("Waiting for connections...")
39
+	
40
+	sessionCounter := 0
41
+	for {
42
+		conn, _ := ln.Accept()
43
+		c := funmow.NewClient(conn, sessionCounter, e, world)
44
+		sessionCounter++
45
+		go c.Run()
46
+	}
47
+}
48
+
49
+func seedDB(world *funmow.Store) {
50
+	outside, _ := world.Allocate("room")
51
+	outside.Name = "outside"
52
+	outside.Description = "The great outdoors. It's scary out here."
53
+	outside.Commit()
54
+
55
+	ketamine, _ := world.Allocate("thing")
56
+	ketamine.Name = "ketamine"
57
+	ketamine.Description = "You see a small pill bottle containing roughly 20 ketamine."
58
+	ketamine.Commit()
59
+	outside.Contains(&ketamine)
60
+
61
+	yacht, _ := world.Allocate("thing")
62
+	yacht.Name = "gravy yacht"
63
+	yacht.Description = "A gravy yacht. Like a boat but bigger. Duh."
64
+	yacht.Commit()
65
+	outside.Contains(&yacht)
66
+
67
+	kitchen, _ := world.Allocate("room")
68
+	kitchen.Name = "kitchen"
69
+	kitchen.Description = "a lovely kitchen"
70
+	kitchen.Commit()
71
+
72
+	kettle, _ := world.Allocate("thing")
73
+	kettle.Name = "kettle"
74
+	kettle.Description = "a lovely little kettle"
75
+	kettle.Commit()
76
+	kitchen.Contains(&kettle)
77
+
78
+	bathroom, _ := world.Allocate("room")
79
+	bathroom.Name = "bathroom"
80
+	bathroom.Description = "a dirty bathroom"
81
+	bathroom.Commit()
82
+
83
+	toilet, _ := world.Allocate("thing")
84
+	toilet.Name = "toilet"
85
+	toilet.Description = "a nasty toilet"
86
+	toilet.Commit()
87
+	bathroom.Contains(&toilet)
88
+
89
+	door1, _ := world.Allocate("exit")
90
+	door1.Name = "<B>athroom;b;bathroom"
91
+	door1.Description = "a door"
92
+	door1.Next = bathroom.ID
93
+	door1.Commit()
94
+
95
+	door2, _ := world.Allocate("exit")
96
+	door2.Name = "<O>utside;o;outside"
97
+	door2.Description = "a door"
98
+	door2.Next = outside.ID
99
+	door2.Commit()
100
+
101
+	door3, _ := world.Allocate("exit")
102
+	door3.Name = "<K>itchen;k;kitchen"
103
+	door3.Description = "a door"
104
+	door3.Next = kitchen.ID
105
+	door3.Commit()
106
+
107
+	door4, _ := world.Allocate("exit")
108
+	door4.Name = "<H>ouse;h;house"
109
+	door4.Description = "a door"
110
+	door4.Next = kitchen.ID
111
+	door4.Commit()
112
+
113
+	kitchen.Contains(&door1)
114
+	kitchen.Contains(&door2)
115
+	bathroom.Contains(&door3)
116
+	outside.Contains(&door4)
117
+
118
+	keelan, _ := world.CreatePlayer("keelan")
119
+	kitchen.Contains(&keelan)
120
+
121
+	dan, _ := world.CreatePlayer("dan")
122
+	bathroom.Contains(&dan)
123
+}

+ 114
- 0
event_distributor.go View File

@@ -0,0 +1,114 @@
1
+package funmow
2
+
3
+const (
4
+	EventTypeEmit  = iota // everyone in the containing object hears it,
5
+	EventTypeOEmit        // everyone in the containing object except the player hears it
6
+	EventTypePEmit        // only the player hears it
7
+	EventTypeWall         // broadcast message
8
+	EventTypePage
9
+	EventTypeSay
10
+	EventTypePose
11
+)
12
+
13
+type PlayerEvent struct {
14
+	src         DBRef
15
+	dst         DBRef
16
+	messageType int
17
+	audience    DBRef
18
+	message     string
19
+}
20
+
21
+type EventDistributor struct {
22
+	subscribeChan chan EventSubscribeRequest
23
+	whoChan       chan chan DBRefList
24
+}
25
+
26
+func NewEventDistributor() *EventDistributor {
27
+	e := new(EventDistributor)
28
+	e.subscribeChan = make(chan EventSubscribeRequest)
29
+	e.whoChan = make(chan chan DBRefList)
30
+	return e
31
+}
32
+
33
+func (e *EventDistributor) Subscribe(subReq EventSubscribeRequest) EventChanSet {
34
+	e.subscribeChan <- subReq
35
+	outbound := <-subReq.chanSet
36
+	return outbound
37
+}
38
+
39
+func (e *EventDistributor) OnlinePlayers() DBRefList {
40
+	replyChan := make(chan DBRefList)
41
+	e.whoChan <- replyChan
42
+	onlinePlayers := <-replyChan
43
+	return onlinePlayers
44
+}
45
+
46
+func (e *EventDistributor) Run() {
47
+
48
+	chanSet := EventChanSet{
49
+		messageChan:  make(chan PlayerEvent),
50
+		shutdownChan: make(chan int),
51
+	}
52
+
53
+	ipcChans := make(map[int]playerRegistration)
54
+
55
+	for {
56
+		select {
57
+		case sub := <-e.subscribeChan:
58
+			ipcChans[sub.connectionID] = playerRegistration{ID: sub.playerID, inbound: sub.inbound}
59
+			sub.chanSet <- chanSet
60
+		case connectionID := <-chanSet.shutdownChan:
61
+			delete(ipcChans, connectionID)
62
+		case message := <-chanSet.messageChan:
63
+			go func() {
64
+				for _, player := range ipcChans {
65
+					switch message.messageType {
66
+					case EventTypeEmit:
67
+						player.inbound <- message // looks like a wall because this goroutine doesn't know where the player is
68
+					case EventTypeOEmit:
69
+						if message.dst != player.ID {
70
+							player.inbound <- message
71
+						}
72
+					case EventTypePEmit:
73
+						if message.dst == player.ID {
74
+							player.inbound <- message
75
+						}
76
+					case EventTypeWall:
77
+						player.inbound <- message
78
+					case EventTypePage:
79
+						if message.dst == player.ID {
80
+							player.inbound <- message
81
+						}
82
+					case EventTypeSay:
83
+						player.inbound <- message
84
+					case EventTypePose:
85
+						player.inbound <- message
86
+					}
87
+				}
88
+			}()
89
+		case replyChan := <-e.whoChan:
90
+			onlinePlayers := make(DBRefList, 0)
91
+			for _, player := range ipcChans {
92
+				onlinePlayers = append(onlinePlayers, player.ID)
93
+			}
94
+			replyChan <- onlinePlayers
95
+		}
96
+	}
97
+}
98
+
99
+type playerRegistration struct {
100
+	ID      DBRef
101
+	inbound chan PlayerEvent
102
+}
103
+
104
+type EventSubscribeRequest struct {
105
+	connectionID int
106
+	playerID     DBRef
107
+	inbound      chan PlayerEvent
108
+	chanSet      chan EventChanSet
109
+}
110
+
111
+type EventChanSet struct {
112
+	messageChan  chan PlayerEvent
113
+	shutdownChan chan int
114
+}

+ 195
- 0
object.go View File

@@ -0,0 +1,195 @@
1
+package funmow
2
+
3
+import (
4
+	"fmt"
5
+	"sort"
6
+	"strings"
7
+)
8
+
9
+const (
10
+	MatchNone = iota
11
+	MatchOne
12
+	MatchMany
13
+)
14
+
15
+type Object struct {
16
+	ID          DBRef                      `json:"id"`
17
+	Next        DBRef                      `json:"next"`
18
+	Type        string                     `json:"type"`
19
+	Name        string                     `json:"name"`
20
+	Description string                     `json:"description"`
21
+	Links       map[string]map[string]bool `json:"links"`
22
+	Inside      DBRef                      `json:"inside"`
23
+	Owner       DBRef                      `json:"owner"`
24
+	world       *Store
25
+}
26
+
27
+func (o *Object) Commit() error {
28
+	return o.world.StoreObject(o)
29
+}
30
+
31
+func (o *Object) Refresh() bool {
32
+	found := o.world.RetrieveObject(o)
33
+	return found
34
+}
35
+
36
+func (o *Object) Link(c *Object) {
37
+	if o.Links == nil {
38
+		o.Links = make(map[string]map[string]bool)
39
+	}
40
+	if o.Links[c.Type] == nil {
41
+		o.Links[c.Type] = make(map[string]bool)
42
+	}
43
+	o.Links[c.Type][c.ID.String()] = true
44
+}
45
+
46
+func (o *Object) Unlink(c *Object) {
47
+	if o.Links == nil || o.Links[c.Type] == nil {
48
+		return
49
+	}
50
+	delete(o.Links[c.Type], c.ID.String())
51
+}
52
+
53
+func (o Object) String() string {
54
+	return fmt.Sprintf("id %d: %s", o.ID, o.Name)
55
+}
56
+
57
+func (o *Object) Remove(c *Object) error {
58
+	return o.world.Unlink(o.ID, c.ID)
59
+}
60
+
61
+func (o *Object) Contains(c *Object) error {
62
+	return o.world.Link(o.ID, c.ID)
63
+}
64
+
65
+func (o *Object) MatchLinkNames(matchName string, player DBRef, excludePlayer bool) ObjectList {
66
+	if matchName == "here" {
67
+		return ObjectList{*o}
68
+	}
69
+	if !excludePlayer && matchName == "me" {
70
+		p, found := o.world.Fetch(player)
71
+		if !found {
72
+			return ObjectList{}
73
+		} else {
74
+			return ObjectList{p}
75
+		}
76
+	}
77
+	r := make(ObjectList, 0)
78
+	if o.Links == nil {
79
+		return r
80
+	}
81
+	for _, links := range o.Links {
82
+		if links == nil {
83
+			continue
84
+		}
85
+		for id, _ := range links {
86
+			v, _ := NewDBRefFromString(id)
87
+			if excludePlayer && player == v {
88
+				continue
89
+			}
90
+			o, found := o.world.Fetch(v)
91
+			if found {
92
+				idName := fmt.Sprintf("#%d", o.ID)
93
+				lowerObjectName := strings.ToLower(o.Name)
94
+				lowerMatchName := strings.ToLower(matchName)
95
+				if strings.HasPrefix(lowerObjectName, lowerMatchName) || matchName == idName {
96
+					r = append(r, o)
97
+				}
98
+			}
99
+		}
100
+	}
101
+	return r
102
+}
103
+
104
+func (o *Object) MatchExitNames(name string) ObjectList { // so much copypasta
105
+	r := make(ObjectList, 0)
106
+	if o.Links == nil {
107
+		return r
108
+	}
109
+	for _, links := range o.Links {
110
+		if links == nil {
111
+			continue
112
+		}
113
+		for id, _ := range links {
114
+			v, _ := NewDBRefFromString(id)
115
+			o, found := o.world.Fetch(v)
116
+			if found {
117
+				idName := fmt.Sprintf("#%d", v)
118
+
119
+				if strings.EqualFold(o.Name, name) || name == idName {
120
+					r = append(r, o)
121
+				}
122
+
123
+				aliases := strings.FieldsFunc(o.Name, func(c rune) bool {
124
+					return c == ';'
125
+				})
126
+				for _, v := range aliases {
127
+					if v == name {
128
+						r = append(r, o)
129
+					}
130
+				}
131
+
132
+			}
133
+		}
134
+	}
135
+	return r
136
+}
137
+
138
+func (o *Object) DetailedName() string {
139
+	return fmt.Sprintf("%s (#%d)", o.Name, o.ID)
140
+}
141
+
142
+func (o *Object) GetLinkNames(matchType string, exclude DBRefList) []string {
143
+	r := make([]string, 0)
144
+	if o.Links == nil {
145
+		return r
146
+	}
147
+	for linkType, links := range o.Links {
148
+		if matchType != "*" && matchType != linkType {
149
+			continue
150
+		}
151
+		if links == nil {
152
+			continue
153
+		}
154
+		for id, _ := range links {
155
+			v, _ := NewDBRefFromString(id)
156
+			skip := false
157
+			for _, excludeID := range exclude {
158
+				if excludeID == v {
159
+					skip = true
160
+				}
161
+			}
162
+			if skip {
163
+				continue
164
+			}
165
+			o, found := o.world.Fetch(v)
166
+			if found {
167
+				r = append(r, o.DetailedName())
168
+			}
169
+		}
170
+	}
171
+	sort.Strings(r)
172
+	return r
173
+}
174
+
175
+type ObjectList []Object
176
+
177
+func (l ObjectList) First() Object {
178
+	if len(l) > 0 {
179
+		return l[0]
180
+	} else {
181
+		return Object{}
182
+	}
183
+}
184
+
185
+func (l ObjectList) ExactlyOne() (Object, int) {
186
+	c := len(l)
187
+
188
+	if c == 0 {
189
+		return Object{}, MatchNone
190
+	} else if c == 1 {
191
+		return l[0], MatchOne
192
+	} else {
193
+		return Object{}, MatchMany
194
+	}
195
+}

+ 308
- 0
world.go View File

@@ -0,0 +1,308 @@
1
+package funmow
2
+
3
+import (
4
+	"encoding/binary"
5
+	"encoding/json"
6
+	"errors"
7
+	"fmt"
8
+	"github.com/boltdb/bolt"
9
+	"strconv"
10
+)
11
+
12
+type DBRef int
13
+
14
+func NewDBRefFromHashRef(v string) (DBRef, error) {
15
+	var destInt int
16
+	_, err := fmt.Sscanf(v, "#%d", &destInt)
17
+
18
+	if err != nil {
19
+		return 0, err
20
+	}
21
+
22
+	return DBRef(destInt), nil
23
+}
24
+
25
+func NewDBRefFromString(v string) (DBRef, error) {
26
+	intVal, err := strconv.Atoi(v)
27
+	return DBRef(intVal), err
28
+}
29
+
30
+func (r DBRef) Valid() bool {
31
+	return r != 0
32
+}
33
+
34
+func (r DBRef) String() string {
35
+	return strconv.Itoa(int(r))
36
+}
37
+
38
+type DBRefList []DBRef
39
+
40
+func (l DBRefList) First() DBRef {
41
+	if len(l) > 0 {
42
+		return l[0]
43
+	} else {
44
+		return 0
45
+	}
46
+}
47
+
48
+type Store struct {
49
+	path string
50
+	db   *bolt.DB
51
+}
52
+
53
+func NewDB(path string) *Store {
54
+	return &Store{path: path}
55
+}
56
+
57
+func (s *Store) Close() {
58
+	s.db.Close()
59
+}
60
+
61
+func (s *Store) Open() error {
62
+	var err error
63
+	s.db, err = bolt.Open(s.path, 0600, nil)
64
+	if err != nil {
65
+		return err
66
+	}
67
+	return s.db.Update(func(tx *bolt.Tx) error {
68
+		var err error
69
+		_, err = tx.CreateBucketIfNotExists([]byte("object"))
70
+		if err != nil {
71
+			return fmt.Errorf("create bucket: %s", err)
72
+		}
73
+		_, err = tx.CreateBucketIfNotExists([]byte("player"))
74
+		if err != nil {
75
+			return fmt.Errorf("create bucket: %s", err)
76
+		}
77
+		return nil
78
+	})
79
+}
80
+
81
+func (s *Store) Link(newContainerRef DBRef, objectRef DBRef) error {
82
+
83
+	return s.db.Update(func(tx *bolt.Tx) error {
84
+		// TODO: if this transaction fails, it will leave the local objects in an
85
+		// inconsistent altered state, which isn't cool.
86
+		b := tx.Bucket([]byte("object"))
87
+
88
+		o := &Object{ID: objectRef}
89
+
90
+		if !txRetrieveObject(b, o) {
91
+			return errors.New("target object does not exist")
92
+		}
93
+
94
+		newContainer := &Object{ID: newContainerRef}
95
+
96
+		if !txRetrieveObject(b, newContainer) {
97
+			return errors.New("destination object does not exist")
98
+		}
99
+
100
+		haveOldContainer := false
101
+		oldContainer := &Object{ID: o.Inside}
102
+		if o.Inside.Valid() {
103
+			haveOldContainer = txRetrieveObject(b, oldContainer)
104
+			if haveOldContainer {
105
+				oldContainer.Unlink(o)
106
+			}
107
+		}
108
+
109
+		newContainer.Link(o)
110
+
111
+		o.Inside = newContainer.ID
112
+
113
+		err := txStoreObject(b, o)
114
+		if err != nil {
115
+			return err
116
+		}
117
+
118
+		if haveOldContainer {
119
+			err = txStoreObject(b, oldContainer)
120
+			if err != nil {
121
+				return err
122
+			}
123
+		}
124
+
125
+		err = txStoreObject(b, newContainer)
126
+		if err != nil {
127
+			return err
128
+		}
129
+
130
+		return nil
131
+	})
132
+}
133
+
134
+func (s *Store) Unlink(newContainerRef DBRef, objectRef DBRef) error {
135
+
136
+	return s.db.Update(func(tx *bolt.Tx) error {
137
+		// TODO: if this transaction fails, it will leave the local objects in an
138
+		// inconsistent altered state, which isn't cool.
139
+		b := tx.Bucket([]byte("object"))
140
+
141
+		o := &Object{ID: objectRef}
142
+
143
+		if !txRetrieveObject(b, o) {
144
+			return errors.New("target object does not exist")
145
+		}
146
+
147
+		if o.Inside.Valid() {
148
+			oldContainer := &Object{ID: o.Inside}
149
+			if txRetrieveObject(b, oldContainer) {
150
+				oldContainer.Unlink(o)
151
+
152
+				o.Inside = 0
153
+
154
+				err := txStoreObject(b, o)
155
+				if err != nil {
156
+					return err
157
+				}
158
+
159
+				err = txStoreObject(b, oldContainer)
160
+				if err != nil {
161
+					return err
162
+				}
163
+			}
164
+		}
165
+		return nil
166
+	})
167
+}
168
+
169
+func (s *Store) Allocate(t string) (Object, error) {
170
+	var o Object
171
+	err := s.db.Update(func(tx *bolt.Tx) error {
172
+		b := tx.Bucket([]byte("object"))
173
+		id, err := b.NextSequence()
174
+		if err != nil {
175
+			return err
176
+		}
177
+		o.ID = DBRef(id)
178
+		o.Type = t
179
+		return txStoreObject(b, &o)
180
+	})
181
+	o.world = s
182
+	return o, err
183
+}
184
+
185
+func (s *Store) Fetch(r DBRef) (Object, bool) {
186
+	o := Object{ID: r}
187
+	found := s.RetrieveObject(&o)
188
+	return o, found
189
+}
190
+
191
+func (s *Store) GetName(r DBRef) string {
192
+	if r == 0 {
193
+		return ""
194
+	} else {
195
+		o := Object{ID: r}
196
+		if s.RetrieveObject(&o) {
197
+			return o.DetailedName()
198
+		} else {
199
+			return fmt.Sprintf("#%d MISSING", r)
200
+		}
201
+	}
202
+}
203
+
204
+func (s *Store) DumpObject(r DBRef) string {
205
+	var dump string
206
+	s.db.View(func(tx *bolt.Tx) error {
207
+		b := tx.Bucket([]byte("object"))
208
+		dump = string(b.Get(itob(int(r))))
209
+		return nil
210
+	})
211
+	return dump
212
+}
213
+
214
+func (s *Store) RetrieveObject(o *Object) bool {
215
+	found := false
216
+	s.db.View(func(tx *bolt.Tx) error { // thats right i throw away an error
217
+		b := tx.Bucket([]byte("object"))
218
+		found = txRetrieveObject(b, o)
219
+		return nil
220
+	})
221
+	o.world = s
222
+	return found
223
+}
224
+
225
+func (s *Store) StoreObject(o *Object) error {
226
+	return s.db.Update(func(tx *bolt.Tx) error {
227
+		b := tx.Bucket([]byte("object"))
228
+		return txStoreObject(b, o)
229
+	})
230
+}
231
+
232
+func (s *Store) SetPlayerID(playername string, id DBRef) error {
233
+	err := s.db.Update(func(tx *bolt.Tx) error {
234
+		b := tx.Bucket([]byte("player"))
235
+		if b == nil {
236
+			return errors.New("Player bucket not found")
237
+		}
238
+		return b.Put([]byte(playername), []byte(id.String()))
239
+	})
240
+	return err
241
+}
242
+
243
+func (s *Store) GetPlayerID(playername string) (DBRef, error) {
244
+	var id DBRef
245
+	err := s.db.View(func(tx *bolt.Tx) error {
246
+		var err error
247
+		b := tx.Bucket([]byte("player"))
248
+		if b == nil {
249
+			return errors.New("Player bucket not found")
250
+		}
251
+		v := b.Get([]byte(playername))
252
+		if v == nil {
253
+			return errors.New("Player not found")
254
+		}
255
+		id, err = NewDBRefFromString(string(v))
256
+		return err
257
+	})
258
+	return id, err
259
+}
260
+
261
+func (s *Store) CreatePlayer(name string) (Object, error) {
262
+	player, err := s.Allocate("player")
263
+	if err != nil {
264
+		return player, err
265
+	}
266
+	player.Name = name
267
+	player.Owner = player.ID
268
+	player.Commit()
269
+	err = s.SetPlayerID(name, player.ID)
270
+	return player, err
271
+}
272
+
273
+func txRetrieveObject(b *bolt.Bucket, o *Object) bool {
274
+	v := b.Get(itob(int(o.ID)))
275
+	if v == nil {
276
+		return false // doesn't exist
277
+	}
278
+	//fmt.Println("txRetrieveObject", o.ID.String(), string(v))
279
+	err := json.Unmarshal(v, o)
280
+	if err != nil {
281
+		// We should log this. the object exists but can't be de-serialized.
282
+		// We'll just pretend it doesn't exist for now.
283
+		return false
284
+	}
285
+	return true
286
+}
287
+
288
+func txStoreObject(b *bolt.Bucket, o *Object) error {
289
+	buf, err := json.Marshal(o)
290
+	//fmt.Println("txStoreObject", o.ID.String(), string(buf))
291
+	if err != nil {
292
+		fmt.Println(err)
293
+		return err
294
+	}
295
+	err = b.Put(itob(int(o.ID)), buf)
296
+	if err != nil {
297
+		fmt.Println(err)
298
+		return err
299
+	}
300
+	return nil
301
+}
302
+
303
+// itob returns an 8-byte big endian representation of v.
304
+func itob(v int) []byte {
305
+	b := make([]byte, 8)
306
+	binary.BigEndian.PutUint64(b, uint64(v))
307
+	return b
308
+}