carbonmu 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +3 -0
  3. data/.rspec +2 -0
  4. data/.ruby-gemset +1 -0
  5. data/.ruby-version +1 -0
  6. data/.travis.yml +14 -0
  7. data/Gemfile +3 -0
  8. data/Gemfile.lock +161 -0
  9. data/Guardfile +50 -0
  10. data/LICENSE.txt +21 -0
  11. data/README.md +66 -0
  12. data/Rakefile +10 -0
  13. data/carbonmu.gemspec +64 -0
  14. data/config/i18n-tasks.yml +89 -0
  15. data/config/locales/en.yml +6 -0
  16. data/config/locales/nl.yml +6 -0
  17. data/console +9 -0
  18. data/doc/architecture.png +0 -0
  19. data/lib/carbonmu.rb +42 -0
  20. data/lib/commands/locale_command.rb +12 -0
  21. data/lib/commands/ping_command.rb +9 -0
  22. data/lib/commands/reboot_command.rb +9 -0
  23. data/lib/commands/say_command.rb +10 -0
  24. data/lib/commands/unknown_command.rb +7 -0
  25. data/lib/core/command.rb +26 -0
  26. data/lib/core/command_context.rb +12 -0
  27. data/lib/core/configuration.rb +22 -0
  28. data/lib/core/connection.rb +19 -0
  29. data/lib/core/internationalization.rb +16 -0
  30. data/lib/core/parser.rb +29 -0
  31. data/lib/core/server.rb +118 -0
  32. data/lib/core/server_supervision_group.rb +5 -0
  33. data/lib/core_ext/match_data.rb +7 -0
  34. data/lib/core_ext/string.rb +5 -0
  35. data/lib/edge_router/edge_connection.rb +39 -0
  36. data/lib/edge_router/edge_router.rb +104 -0
  37. data/lib/edge_router/edge_router_supervision_group.rb +5 -0
  38. data/lib/edge_router/telnet_connection.rb +35 -0
  39. data/lib/edge_router/telnet_receptor.rb +30 -0
  40. data/lib/errors.rb +4 -0
  41. data/lib/game_objects/container.rb +10 -0
  42. data/lib/game_objects/exit.rb +8 -0
  43. data/lib/game_objects/game_object.rb +13 -0
  44. data/lib/game_objects/movable.rb +12 -0
  45. data/lib/game_objects/player.rb +17 -0
  46. data/lib/game_objects/room.rb +16 -0
  47. data/lib/game_objects/thing.rb +7 -0
  48. data/lib/interactions/notify.rb +11 -0
  49. data/lib/interactions/reboot.rb +8 -0
  50. data/lib/ipc/carbon_ipc_socket.rb +21 -0
  51. data/lib/ipc/ipc_message.rb +54 -0
  52. data/lib/ipc/read_socket.rb +18 -0
  53. data/lib/ipc/write_socket.rb +12 -0
  54. data/lib/version.rb +7 -0
  55. data/mongoid.yml +12 -0
  56. data/spec/carbonmu_spec.rb +14 -0
  57. data/spec/i18n_spec.rb +18 -0
  58. data/spec/lib/commands/say_command_spec.rb +11 -0
  59. data/spec/lib/core/command_context_spec.rb +16 -0
  60. data/spec/lib/core/command_spec.rb +37 -0
  61. data/spec/lib/core/configuration_spec.rb +37 -0
  62. data/spec/lib/core/connection_spec.rb +46 -0
  63. data/spec/lib/core/internationalization_spec.rb +24 -0
  64. data/spec/lib/core/parser_spec.rb +30 -0
  65. data/spec/lib/core/server_spec.rb +35 -0
  66. data/spec/lib/edge_router/edge_connection_spec.rb +9 -0
  67. data/spec/lib/game_objects/container_spec.rb +9 -0
  68. data/spec/lib/game_objects/exit_spec.rb +7 -0
  69. data/spec/lib/game_objects/game_object_spec.rb +12 -0
  70. data/spec/lib/game_objects/movable_spec.rb +9 -0
  71. data/spec/lib/game_objects/player_spec.rb +24 -0
  72. data/spec/lib/game_objects/room_spec.rb +20 -0
  73. data/spec/lib/game_objects/thing_spec.rb +7 -0
  74. data/spec/lib/interactions/notify_spec.rb +26 -0
  75. data/spec/lib/interactions/reboot_spec.rb +9 -0
  76. data/spec/lib/ipc/carbon_ipc_socket_spec.rb +12 -0
  77. data/spec/lib/ipc/ipc_message_spec.rb +39 -0
  78. data/spec/lib/ipc/read_socket_spec.bak +51 -0
  79. data/spec/lib/ipc/write_socket_spec.bak +25 -0
  80. data/spec/spec_helper.rb +118 -0
  81. data/spec/support/helpers.rb +15 -0
  82. data/spec/support/matchers/be_boolean.rb +5 -0
  83. data/spec/support/shared_examples/carbon_ipc_socket.rb +3 -0
  84. data/spec/support/shared_examples/container.rb +3 -0
  85. data/spec/support/shared_examples/game_object.rb +6 -0
  86. data/spec/support/shared_examples/movable.rb +4 -0
  87. data/start +5 -0
  88. data/start-server-only +8 -0
  89. metadata +416 -0
@@ -0,0 +1,6 @@
1
+ en:
2
+ emits:
3
+ say: '%{name} says, "%{message}"'
4
+ pose: '%{name} %{message}'
5
+ semipose: '%{name}%{message}'
6
+ emit: '<%{name}> %{message}'
@@ -0,0 +1,6 @@
1
+ nl:
2
+ emits:
3
+ say: '%{name} zegt, "%{message}"'
4
+ pose: '%{name} %{message}'
5
+ semipose: '%{name}%{message}'
6
+ emit: '<%{name}> %{message}'
data/console ADDED
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env ruby
2
+ # Console for testing. TODO replace me with a proper cli.
3
+
4
+ require './lib/carbonmu.rb'
5
+ require 'awesome_print'
6
+ require 'pry'
7
+
8
+ CarbonMU::Server.initialize_database
9
+ Pry.start CarbonMU
Binary file
@@ -0,0 +1,42 @@
1
+ require 'rubygems'
2
+ require 'bundler/setup'
3
+
4
+ require 'require_all'
5
+ require 'colorize' # An exception to our require-where-used rule, since it's likely to be used many places.
6
+ require_all 'lib'
7
+
8
+ Celluloid::ZMQ.init
9
+
10
+ module CarbonMU
11
+ class << self
12
+ attr_accessor :configuration
13
+ attr_accessor :edge_router_receive_port
14
+ attr_accessor :server
15
+ end
16
+ self.configuration = Configuration.new
17
+
18
+ def self.configure
19
+ yield self.configuration
20
+ end
21
+
22
+ def self.start
23
+ EdgeRouterSupervisionGroup.run
24
+ end
25
+
26
+ def self.start_in_background
27
+ EdgeRouterSupervisionGroup.run!
28
+ end
29
+
30
+ def self.start_server
31
+ ServerSupervisionGroup.run
32
+ end
33
+
34
+ def self.start_server_in_background
35
+ ServerSupervisionGroup.run!
36
+ end
37
+
38
+ def self.t(str, *opts)
39
+ CarbonMU::Internationalization.translate(str, opts)
40
+ end
41
+ end
42
+
@@ -0,0 +1,12 @@
1
+ # TODO TEMP
2
+ module CarbonMU
3
+ class LocaleCommand < Command
4
+ syntax /^locale (?<locale>.*)/
5
+ syntax /^do (?<text>.*)/
6
+
7
+ def execute
8
+ @context.enacting_connection.locale = @params[:locale]
9
+ Notify.one(@context.enacting_connection, "Locale changed to #{@params[:locale]}.")
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,9 @@
1
+ module CarbonMU
2
+ class PingCommand < Command
3
+ syntax "ping"
4
+
5
+ def execute
6
+ Notify.one(@context.enacting_connection, "PONG")
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ module CarbonMU
2
+ class RebootCommand < Command
3
+ syntax "reboot"
4
+
5
+ def execute
6
+ Reboot.reboot
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,10 @@
1
+ module CarbonMU
2
+ class SayCommand < Command
3
+ syntax /^say (?<text>.*)/
4
+ syntax /^do (?<text>.*)/
5
+
6
+ def execute
7
+ Notify.all("emits.say", {name: @context.enactor.name, message: @params[:text].light_white})
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,7 @@
1
+ module CarbonMU
2
+ class UnknownCommand < Command
3
+ def execute
4
+ Notify.one(@context.enacting_connection, "Unknown command: #{@context.raw_command}")
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,26 @@
1
+ module CarbonMU
2
+ class Command
3
+ class << self
4
+ def syntax(value)
5
+ @syntaxes ||= []
6
+ @syntaxes << value
7
+ Parser.register_syntax(value, self)
8
+ end
9
+
10
+ def syntaxes
11
+ @syntaxes
12
+ end
13
+ end
14
+
15
+ attr_reader :context
16
+
17
+ def initialize(context)
18
+ @context = context
19
+ @params = context.params
20
+ end
21
+
22
+ def execute
23
+ raise NotImplementedError
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,12 @@
1
+ module CarbonMU
2
+ class CommandContext
3
+ attr_reader :enacting_connection, :enactor, :raw_command, :params
4
+
5
+ def initialize(opts)
6
+ @enacting_connection = opts[:enacting_connection] || nil
7
+ @enactor = @enacting_connection.player || nil
8
+ @raw_command = opts[:raw_command] || ""
9
+ @params = opts[:params] || {}
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,22 @@
1
+ require 'celluloid'
2
+ require 'logger'
3
+
4
+ module CarbonMU
5
+ class Configuration
6
+ attr_accessor :game_name
7
+ attr_reader :logger
8
+ attr_accessor :log_ipc_traffic
9
+
10
+ def initialize
11
+ @game_name = 'CarbonMU'
12
+ @logger = Celluloid.logger.tap { |i| i.progname = @game_name }
13
+ @log_ipc_traffic = true
14
+ end
15
+
16
+ def logger=(logger)
17
+ raise ArgumentError.new("Not a logger: #{logger}") unless logger.is_a?(Logger)
18
+ Celluloid.logger = logger
19
+ @logger = logger
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,19 @@
1
+ module CarbonMU
2
+ class Connection
3
+ attr_reader :id
4
+ attr_accessor :locale
5
+ attr_accessor :player
6
+
7
+ # TODO probably need edge type (:telnet, :ssl, etc), or at least interactive true/false
8
+ def initialize(id)
9
+ @id = id
10
+ @locale = "en" # TODO configurable default locale
11
+ @player = nil
12
+ end
13
+
14
+ def write_translated(msg, args = {})
15
+ translation_args = args.merge(locale: @locale)
16
+ CarbonMU.server.write_to_connection(id, I18n.t(msg, translation_args) + "\n")
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,16 @@
1
+ require "i18n"
2
+ require "i18n/backend/fallbacks"
3
+
4
+ module CarbonMU
5
+ module Internationalization
6
+
7
+ def self.setup
8
+ I18n.enforce_available_locales = false
9
+ I18n.load_path = Dir["config/locales/**/*.yml"] # TODO support for plugins too.
10
+ end
11
+
12
+ def self.translate(str, *opts)
13
+ I18n.t(str, opts)
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,29 @@
1
+ module CarbonMU
2
+ module Parser
3
+ def self.parse_and_execute(enacting_connection, input)
4
+ command_class, params = parse(input)
5
+
6
+ context = CommandContext.new(enacting_connection: enacting_connection, raw_command: input, params: params)
7
+ command_class.new(context).execute
8
+ end
9
+
10
+ def self.parse(input)
11
+ syntax_match_data = {}
12
+ syntax_and_class = @@syntaxes.detect { |syntax, klass| syntax_match_data = syntax.match(input) }
13
+
14
+ command_class = syntax_and_class.nil? ? UnknownCommand : syntax_and_class[1]
15
+ syntax_match_data ||= {} # if no syntaxes matched
16
+
17
+ [command_class, syntax_match_data.to_hash]
18
+ end
19
+
20
+ def self.register_syntax(syntax, klass)
21
+ @@syntaxes ||= {}
22
+ @@syntaxes[syntax] = klass
23
+ end
24
+
25
+ def self.syntaxes
26
+ @@syntaxes
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,118 @@
1
+ require 'celluloid/io'
2
+ require 'celluloid/zmq'
3
+ require 'mongoid'
4
+
5
+ module CarbonMU
6
+ class Server
7
+ include Celluloid::IO
8
+ include Celluloid::Logger
9
+ include Celluloid::ZMQ
10
+
11
+ finalizer :shutdown
12
+
13
+ attr_reader :connections
14
+
15
+ def initialize(standalone = false)
16
+ Internationalization.setup
17
+
18
+ info "*** Starting CarbonMU game server to connect to edge router port #{CarbonMU.edge_router_receive_port}."
19
+ CarbonMU.server = Actor.current
20
+
21
+ Server.initialize_database
22
+ Server.create_starter_objects
23
+
24
+ unless standalone
25
+ @ipc_reader = ReadSocket.new
26
+ @ipc_writer = WriteSocket.new(CarbonMU.edge_router_receive_port)
27
+ send_server_started_to_edge_router
28
+
29
+ async.run
30
+
31
+ retrieve_existing_connections
32
+ end
33
+ end
34
+
35
+ def run
36
+ loop do
37
+ async.handle_edge_router_message(@ipc_reader.read)
38
+ end
39
+ end
40
+
41
+ def shutdown
42
+ error "Terminating server!"
43
+ end
44
+
45
+ def add_connection(connection_id)
46
+ @connections ||= []
47
+ c = Connection.new(connection_id)
48
+ c.player = Player.superadmin #TODO real login
49
+ @connections << c
50
+ end
51
+
52
+ def remove_connection(connection_id)
53
+ @connections.delete_if { |c| c.id == connection_id }
54
+ end
55
+
56
+ def handle_command(input, connection_id)
57
+ connection = @connections.find { |c| c.id == connection_id }
58
+ Parser.parse_and_execute(connection, input)
59
+ end
60
+
61
+ def write_to_connection(connection_id, str)
62
+ send_message_to_edge_router(:write, connection_id: connection_id, output: str)
63
+ end
64
+
65
+ def send_server_started_to_edge_router
66
+ send_message_to_edge_router(:started, port: @ipc_reader.port_number, pid: Process.pid)
67
+ end
68
+
69
+ def handle_edge_router_message(input)
70
+ message = IPCMessage.unserialize(input)
71
+ debug "SERVER RECEIVE: #{message}" if CarbonMU.configuration.log_ipc_traffic
72
+ case message.op
73
+ when :command
74
+ handle_command(message.command, message.connection_id)
75
+ when :connect
76
+ add_connection(message.connection_id)
77
+ when :disconnect
78
+ remove_connection(message.connection_id)
79
+ else
80
+ raise ArgumentError, "Unsupported operation '#{message.op}' received from edge router."
81
+ end
82
+ end
83
+
84
+ def retrieve_existing_connections
85
+ send_message_to_edge_router(:retrieve_existing_connections)
86
+ end
87
+
88
+ def self.trigger_reboot
89
+ Actor[:server].send_reboot_message_to_edge_router
90
+ end
91
+
92
+ def send_reboot_message_to_edge_router
93
+ send_message_to_edge_router(:reboot)
94
+ end
95
+
96
+ def send_message_to_edge_router(op, params={})
97
+ message = IPCMessage.new(op, params)
98
+ debug "SERVER SEND: #{message}" if CarbonMU.configuration.log_ipc_traffic
99
+ @ipc_writer.send message.serialize
100
+ end
101
+
102
+ def self.initialize_database
103
+ Mongoid.logger.level = ::Logger::DEBUG
104
+ Mongoid.load!("mongoid.yml", ENV["MONGOID_ENV"] || :production)
105
+ ::Mongoid::Tasks::Database.create_indexes
106
+ end
107
+
108
+ def self.create_starter_objects
109
+ ensure_special_exists(:starting_room, Room, {name: "Starting Room", description: "This is the starting room for newly-created players. Feel free to rename and re-describe it."})
110
+ ensure_special_exists(:lostandfound_room, Room, {name: "Lost & Found Room", description: "This is the room where objects and players go if the thing that was holding them gets destroyed."})
111
+ ensure_special_exists(:superadmin_player, Player, {name: "Superadmin", description: "Obviously the most powerful of his race, it could kill us all."})
112
+ end
113
+
114
+ def self.ensure_special_exists(special, klass, attributes)
115
+ klass.create!(attributes.merge(_special: special)) if klass.where(_special: special).count == 0
116
+ end
117
+ end
118
+ end
@@ -0,0 +1,5 @@
1
+ module CarbonMU
2
+ class ServerSupervisionGroup < Celluloid::SupervisionGroup
3
+ supervise Server, as: :server
4
+ end
5
+ end
@@ -0,0 +1,7 @@
1
+ class MatchData
2
+ def to_hash
3
+ ret = Hash.new
4
+ names.each { |k| ret[k.to_sym] = self[k] }
5
+ return ret
6
+ end
7
+ end
@@ -0,0 +1,5 @@
1
+ class String
2
+ def collectionize
3
+ split(":").last.tableize # Don't put module name in default Mongoid collection names.
4
+ end
5
+ end
@@ -0,0 +1,39 @@
1
+ module CarbonMU
2
+ class EdgeConnection
3
+ include Celluloid::IO
4
+ include Celluloid::Logger
5
+
6
+ attr_accessor :name
7
+ attr_reader :id
8
+
9
+ finalizer :shutdown
10
+
11
+ def initialize(arg = nil)
12
+ @id = SecureRandom.uuid
13
+ after_initialize(arg)
14
+ end
15
+
16
+ def after_initialize(arg)
17
+ nil # to be implemented by subclasses, if desired
18
+ end
19
+
20
+ def handle_input(input)
21
+ input.chomp!
22
+ Actor[:edge_router].async.send_command_to_server(input, id)
23
+ end
24
+
25
+ def run
26
+ raise NotImplementedError
27
+ end
28
+
29
+ def shutdown
30
+ before_shutdown
31
+ Actor[:edge_router].async.remove_connection(Actor.current)
32
+ end
33
+
34
+ def before_shutdown
35
+ nil # to be implemented by subclasses, if desired
36
+ end
37
+ end
38
+ end
39
+
@@ -0,0 +1,104 @@
1
+ require 'celluloid/autostart'
2
+ require 'celluloid/io'
3
+ require 'celluloid/zmq'
4
+
5
+ module CarbonMU
6
+ class EdgeRouter
7
+ include Celluloid::IO
8
+ include Celluloid::Logger
9
+ include Celluloid::ZMQ
10
+
11
+ finalizer :shutdown
12
+
13
+ attr_reader :receptors, :connections
14
+
15
+ def initialize(host, port)
16
+ info "*** Starting CarbonMU edge router."
17
+ @receptors = TelnetReceptor.new(host,port)
18
+ @connections = []
19
+
20
+ @ipc_reader = ReadSocket.new
21
+ info "*** Edge router waiting for IPC on port #{@ipc_reader.port_number}"
22
+ async.run
23
+
24
+ start_server
25
+ end
26
+
27
+ def start_server
28
+ spawn("/usr/bin/env ruby start-server-only #{@ipc_reader.port_number}")
29
+ end
30
+
31
+ def handle_server_started(pid, port)
32
+ debug "*** Edge router received server IPC start. Pid #{pid}, port #{port}." if CarbonMU.configuration.log_ipc_traffic
33
+ @current_server_pid = pid
34
+ Process.detach(pid)
35
+ @ipc_writer = WriteSocket.new(port)
36
+ end
37
+
38
+ def add_connection(connection)
39
+ @connections << connection
40
+ send_connect_to_server(connection)
41
+ end
42
+
43
+ def remove_connection(connection)
44
+ @connections.delete(connection)
45
+ send_disconnect_to_server(connection)
46
+ end
47
+
48
+ def shutdown
49
+ # TODO Tell all receptors and connections to quit.
50
+ Process.kill("TERM", @current_server_pid)
51
+ end
52
+
53
+ def run
54
+ loop do
55
+ async.handle_server_message(@ipc_reader.read)
56
+ end
57
+ end
58
+
59
+ def send_connect_to_server(connection)
60
+ send_message_to_server(:connect, connection_id: connection.id)
61
+ end
62
+
63
+ def send_disconnect_to_server(connection)
64
+ send_message_to_server(:disconnect, connection_id: connection.id)
65
+ end
66
+
67
+ def send_command_to_server(input, connection_id)
68
+ send_message_to_server(:command, command: input, connection_id: connection_id)
69
+ end
70
+
71
+ def send_message_to_server(op, params={})
72
+ message = IPCMessage.new(op, params)
73
+ debug "EDGE ROUTER SEND: #{message}" if CarbonMU.configuration.log_ipc_traffic
74
+ @ipc_writer.send message.serialize
75
+ end
76
+
77
+ def reboot_server
78
+ warn "Reboot triggered!"
79
+ Process.kill("TERM", @current_server_pid)
80
+ start_server
81
+ end
82
+
83
+ def handle_server_message(input)
84
+ message = IPCMessage.unserialize(input)
85
+ debug "EDGE ROUTER RECEIVE: #{message}" if CarbonMU.configuration.log_ipc_traffic
86
+ case message.op
87
+ when :started
88
+ handle_server_started(message.pid, message.port)
89
+ when :write
90
+ conn = @connections.select {|x| x.id == message.connection_id}.first # TODO look for efficiency here
91
+ conn.write(message.output)
92
+ when :reboot
93
+ reboot_server
94
+ when :retrieve_existing_connections
95
+ info 'Sending connections to server...'
96
+ @connections.each do |conn|
97
+ send_connect_to_server(conn)
98
+ end
99
+ else
100
+ raise ArgumentError, "Unsupported operation '#{message.op}' received from Server."
101
+ end
102
+ end
103
+ end
104
+ end