carbonmu 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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