Class: Pixiurge::EngineConnector
- Inherits:
-
Object
- Object
- Pixiurge::EngineConnector
- Defined in:
- lib/pixiurge/engine_connector.rb
Overview
A single EngineConnector runs on the server, sending messages about the game world to the various player connections. An EngineConnector subscribes to events on a Demiurge engine for the gameworld and a Pixiurge App to connect to browsers.
The EngineConnector acts, among other things, as a mapping between Demiurge objects, websockets, Player objects and Displayable objects.
Constant Summary
- SIMULATION_OPTIONS =
Legal simulation-related hash options for #initialize
[ :engine, :engine_text, :engine_files, :engine_dsl_dir, :engine_restore_statefile, :ms_per_tick, :autosave_ticks, :autosave_path ]
- CONSTRUCTOR_OPTIONS =
Legal hash options for #initialize
SIMULATION_OPTIONS + [ :default_width, :default_height, :no_start_simulation ]
- IGNORED_NOTIFICATION_TYPES =
This is the internal constant to exit early from the notification routine if the notification is of any of these types. Anything here requires no direct response from the EngineConnector, for a variety of different reasons.
{ # Tick finished? Great, no change. Demiurge::Notifications::TickFinished => true, # MoveFrom? We handle the corresponding MoveTo instead. Demiurge::Notifications::MoveFrom => true, # LoadStateStart/Verify or LoadWorldStart notifications? We'll make changes when they complete Demiurge::Notifications::LoadStateStart => true, Demiurge::Notifications::LoadWorldStart => true, Demiurge::Notifications::LoadWorldVerify => true, # Player logout or reconnect? Already handled. This EngineConnector was the object that sent this notification anyway. Pixiurge::Notifications::PlayerLogout => true, Pixiurge::Notifications::PlayerReconnect => true, }
Instance Attribute Summary collapse
- #app ⇒ Object readonly
- #engine ⇒ Object readonly
Instance Method Summary collapse
-
#add_player(player) ⇒ Object
This must be called on the Demiurge timeline, not immediately.
-
#displayable_by_name(item_name) ⇒ Pixiurge::Displayable?
Query for a Displayable object by Demiurge item name.
- #displayable_for_item(item) ⇒ Object
- #each_displayable_for_location_name(location_name, &block) ⇒ Object
- #each_player_for_location_name(location_name, options = { :except => [] }, &block) ⇒ Object
- #hide_displayable_from_players(displayable, position) ⇒ Object
-
#initialize(pixi_app, options = {}) ⇒ EngineConnector
constructor
Constructor.
-
#notified(data) ⇒ Object
When new data comes in about things in the engine changing, this is what receives that notification.
- #notified_of_move_to(data) ⇒ Object
-
#player_by_username(username) ⇒ Pixiurge::Player?
Query for a Player object by account name, which should match the Demiurge username for that player's body if there is one.
-
#register_displayable_object(object) ⇒ void
This method adds a new Displayable object which does not correspond to a Demiurge item.
- #register_engine_item(item) ⇒ Object
-
#remove_player(player) ⇒ Object
The logout action happens before this does, which may affect what's where.
-
#set_player_backdrop(player, player_position, location_do) ⇒ Object
This is going to wind up as a complicated interaction at some point between the player and the zone/location.
- #show_displayable_to_players(displayable, options = { :except => []}) ⇒ Object
-
#start_simulation(options = {}) ⇒ void
Make sure the simulation is running.
-
#thin_eventmachine_loop(rack_app, port, ssl_key_path, ssl_cert_path) ⇒ Object
private
Start the EventMachine loop to run Thin, the periodic timer and so on.
Constructor Details
#initialize(pixi_app, options = {}) ⇒ EngineConnector
Constructor. Create the EngineConnector, which will act as a gateway between the simulation engine and the network and authorization interfaces (a.k.a. "the app".) The engine and any additional settings will be configured after the object is allocated.
Unless the :no_start_simulation option is true, #start_simulation will be called with the appropriate simulation-related options to create the simulation engine.
49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 |
# File 'lib/pixiurge/engine_connector.rb', line 49 def initialize(pixi_app, = {}) = .keys - CONSTRUCTOR_OPTIONS raise("Illegal options passed to EngineConnector#initialize: #{.inspect}!") unless .empty? @app = pixi_app @players = {} # Mapping of player name strings to Player objects (not Displayable objects or Demiurge items) @displayables = {} # Mapping of item names to the original registered source, and display objects such as TileAnimatedSprites @default_width = [:default_width] || 640 @default_height = [:default_height] || 480 @ms_per_tick = 500 unless [:no_start_simulation] # @todo Replace this with Hash#slice and require a higher minimum Ruby version = {} SIMULATION_OPTIONS.each { |opt| .has_key?(opt) && [opt] = [opt] } start_simulation() end end |
Instance Attribute Details
#app ⇒ Object (readonly)
16 17 18 |
# File 'lib/pixiurge/engine_connector.rb', line 16 def app @app end |
#engine ⇒ Object (readonly)
15 16 17 |
# File 'lib/pixiurge/engine_connector.rb', line 15 def engine @engine end |
Instance Method Details
#add_player(player) ⇒ Object
This must be called on the Demiurge timeline, not immediately. So we wait for the PlayerLogin notification.
434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 |
# File 'lib/pixiurge/engine_connector.rb', line 434 def add_player(player) @players[player.name] = player unless player.displayable raise "Set the Player's Displayable before this!" end loc_name = player.displayable.location_name player_position = player.displayable.position loc_do = @displayables[loc_name][:displayable] # Do we have a display object for that player's location? unless loc_do STDERR.puts "This player doesn't seem to be in a known displayable location, instead is in #{loc_name.inspect}!" return end set_player_backdrop(player, player_position, loc_do) end |
#displayable_by_name(item_name) ⇒ Pixiurge::Displayable?
Query for a Displayable object by Demiurge item name. This is useful when trying to map a Demiurge location into a displayable tilemapped area, for instance. It can also query for agents and other non-location Demiurge items - anything with a Displayable presence.
269 270 271 |
# File 'lib/pixiurge/engine_connector.rb', line 269 def displayable_by_name(item_name) @displayables[item_name][:displayable] end |
#displayable_for_item(item) ⇒ Object
295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 |
# File 'lib/pixiurge/engine_connector.rb', line 295 def displayable_for_item(item) return if item.is_a?(Demiurge::InertStateItem) # Nothing needed for InertStateItems # See if the item uses a custom Displayable set via the "display" # block in the World Files. If so, use it. disp_action = item.get_action("$display") if disp_action && disp_action["block"] # This special action is used to pass the Display info through to a Display library. builder = Pixiurge::Display::DisplayBuilder.new(item, engine_connector: self, &(disp_action["block"])) displayables = builder.built_objects raise("Only display one object per agent right now for item #{item.name.inspect}!") if displayables.size > 1 raise("No display objects declared for item #{item.name.inspect}!") if displayables.size == 0 return displayables[0] # Exactly one display object. Perfect. end if item.is_a?(::Demiurge::Tmx::TmxLocation) return ::Pixiurge::Display::TmxMap.new item.tile_cache_entry, name: item.name, engine_connector: self # Build a Pixiurge location elsif item.agent? # No Display information? Default to generic guy in a hat. layers = [ "male", "kettle_hat_male", "robe_male" ] raise "Nope! Haven't implemented a default displayable for agents yet!" end if item.zone? return ::Pixiurge::Display::Invisible.new name: item.name, engine_connector: self end # If we got here, we have no idea how to display this. nil end |
#each_displayable_for_location_name(location_name, &block) ⇒ Object
381 382 383 384 385 386 387 388 |
# File 'lib/pixiurge/engine_connector.rb', line 381 def each_displayable_for_location_name(location_name, &block) @displayables.each do |disp_name, disp_hash| disp = disp_hash[:displayable] if disp.location_name == location_name yield(disp) end end end |
#each_player_for_location_name(location_name, options = { :except => [] }, &block) ⇒ Object
372 373 374 375 376 377 378 379 |
# File 'lib/pixiurge/engine_connector.rb', line 372 def each_player_for_location_name(location_name, = { :except => [] }, &block) @players.each do |player_name, player| next if [:except].include?(player) || [:except].include?(player_name) if player.displayable && player.displayable.location_name == location_name yield(player) end end end |
#hide_displayable_from_players(displayable, position) ⇒ Object
398 399 400 401 402 403 404 405 406 407 408 409 |
# File 'lib/pixiurge/engine_connector.rb', line 398 def hide_displayable_from_players(displayable, position) position ||= displayable.demi_item.position return unless position loc_name = position.split("#")[0] loc = @engine.item_by_name(loc_name) if loc.is_a?(::Demiurge::Tmx::TmxLocation) each_player_for_location_name(loc_name) do |player| player.destroy_displayable(displayable.name) end end end |
#notified(data) ⇒ Object
When new data comes in about things in the engine changing, this is what receives that notification.
479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 |
# File 'lib/pixiurge/engine_connector.rb', line 479 def notified(data) # First, ignore a bunch of notifications we don't care about return if IGNORED_NOTIFICATION_TYPES[data["type"]] # We subscribe to all events in all locations, and the move-from # and move-to have the same fields except location, zone and # type. So only pay attention to the move_to. if data["type"] == Demiurge::Notifications::MoveTo return notified_of_move_to(data) end if data["type"] == Demiurge::Notifications::LoadStateEnd # What to do here? Displayables don't care about this any more. Does anything in Pixiurge? return end # On Login, we let the Player object know it can start showing all # update messages, not just the special early ones. We've caught # up to the point in the notification stream where this player # logged in, so after this it's all actually new. if data["type"] == Pixiurge::Notifications::PlayerLogin username = data["player"] # We touch (and/or create) the body Demiurge item on the assumption that nobody else is using it yet... demi_item = @engine.item_by_name(username) if demi_item raise("There is already a body with reserved name #{username} not marked for this player!") unless demi_item.state["$player_body"] == username else # No body yet? Send a signal to indicate that we need one. @app.send("send_event", "player_create_body", username) demi_item = @engine.item_by_name(username) unless demi_item # Still no body? Either there was no signal handler, or it did nothing. STDERR.puts "No player body was created in Demiurge for #{username.inspect}! No login for you!" return end demi_item.state["$player_body"] = username demi_item.run_action("create") if demi_item.get_action("create") # Now register the player's body with the EngineConnector to make sure we have a Displayable for it register_engine_item(demi_item) displayable = @displayables[username][:displayable] unless(displayable) raise "No displayable item was created for user #{username}'s body!" end end demi_item.run_action("login") if demi_item.get_action("login") ws = @app.websocket_for_username username displayable = @displayables[username][:displayable] player = Pixiurge::Player.new websocket: ws, name: username, displayable: displayable, display_settings: display_settings, engine_connector: self # Add_player sets the player's backdrop manually. add_player(player) return end if data["type"] == Demiurge::Notifications::LoadWorldEnd # Not entirely clear what we do here. Demiurge has just reloaded # the World files, which may result in a bunch of changes... # Though if objects get created, destroyed or moved, that should # get separate notifications which should finish before this # happens. We don't care that they're part of a World Reload -- # if we did, we'd track the LoadWorldStart. return end if data["type"] == Demiurge::Notifications::IntentionCancelled acting_item = data["actor"] if @players[acting_item] # This was a player action that was cancelled player = @players[acting_item] displayable = Pixiurge::Display::TextEffect.new(data["reason"], style: { fill: "orange" }, duration: 3000, name: "", engine_connector: self) displayable.position = player.displayable.position player.show_displayable(displayable) return end return end if data["type"] == Demiurge::Notifications::IntentionApplied # For right now we don't need any kind of confirmations. But # when we do, this is where they come from. return end # This notification will catch new player bodies, instantiated agents and whatnot. if data["type"] == Demiurge::Notifications::NewItem item = @engine.item_by_name data["actor"] register_engine_item(item) return end STDERR.puts "Unhandled notification of type #{data["type"].inspect}...\n#{data.inspect}" end |
#notified_of_move_to(data) ⇒ Object
572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 |
# File 'lib/pixiurge/engine_connector.rb', line 572 def notified_of_move_to(data) actor_do = @displayables[data["actor"]][:displayable] x, y = ::Demiurge::TiledLocation.position_to_coords(data["new_position"]) loc_name = data["new_location"] loc_do = @displayables[loc_name] ? @displayables[loc_name][:displayable] : nil unless loc_do STDERR.puts "Moving to a non-displayed location #{loc_name.inspect}, no display object found..." return end actor_do.position = data["new_position"] # Is it a player that just moved? If so, update them specifically. acting_player = @players[data["actor"]] if acting_player if data["old_location"] != data["new_location"] ## Hide the old location's Displayables and show the new ## location's Displayables to the player who is moving set_player_backdrop(acting_player, data["new_position"], @displayables[loc_name][:displayable]) else # Player moved in same location, pan to new position acting_player.move_displayable(actor_do, data["old_position"], data["new_position"]) acting_player.pan_to_coordinates(x, y) end end # Whether it's a player moving or something else, update all the # players who just saw the item move, disappear or appear. @players.each do |player_name, player| next if player_name == data["actor"] # Already handled it if this player is the one moving. player_loc_name = player.displayable.location_name # First case: moving player remained in the same room - update movement for anybody *in* that room if data["old_location"] == data["new_location"] next unless player_loc_name == data["new_location"] actor_do.move_for_player(player, data["old_position"], data["new_position"], {}) # Second case: moving player changed rooms and we're in the old one elsif player_loc_name == data["old_location"] # The item changed rooms and the player is in the old # location. Hide the item. player.destroy_displayable(actor_do) # Third case: moving player changed rooms and we're in the new one elsif player_loc_name == data["new_location"] # The item changed rooms and the player is in the new # location. Show the item, if it moved to a displayable # location. player.show_displayable(actor_do) end end end |
#player_by_username(username) ⇒ Pixiurge::Player?
Query for a Player object by account name, which should match the Demiurge username for that player's body if there is one.
279 280 281 |
# File 'lib/pixiurge/engine_connector.rb', line 279 def player_by_username(username) @players[username] end |
#register_displayable_object(object) ⇒ void
This method returns an undefined value.
This method adds a new Displayable object which does not correspond to a Demiurge item. This is useful for Displayables for non-Demiurge objects such as dialog boxes or title screens, and for components of larger objects (e.g. a player's shadow) which are attached to a Demiure item but don't really represent a Demiurge item.
Among other things this reserves a name for the Displayable in the EngineConnector. A Displayable isn't allowed to share a name with any other Displayable nor with any item in the Demiurge engine. For this reason, a common convention for "sub-Displayables" is to use the parent Displayable's name followed by an at-sign and a name or number. The at-sign isn't a legal character for Demiurge item names, so this makes it clear what the "attached" item is and guarantees uniqueness.
364 365 366 367 368 369 370 |
# File 'lib/pixiurge/engine_connector.rb', line 364 def register_displayable_object(object) if @displayables[object.name] # Already have this one return if @displayables[object.name][:source] == object # Duplicate registration, which is fine raise "Already have a Displayable named #{object.name.inspect} registered by #{@displayables[object.name][:source].inspect}! Can't re-register as Displayable #{object.inspect}!" end @displayables[object.name] = { displayable: object, source: object } end |
#register_engine_item(item) ⇒ Object
325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 |
# File 'lib/pixiurge/engine_connector.rb', line 325 def register_engine_item(item) if @displayables[item.name] # Already have this one return if @displayables[item.name][:source] == :demiurge # Duplicate registration, it's fine raise "Displayable name #{item.name.inspect} is already used by source object #{@displayables[item.name][:source].inspect}! Can't re-register as #{item.inspect}!" end displayable = displayable_for_item(item) if displayable @displayables[item.name] = { displayable: displayable, source: :demiurge } displayable.position = item.position if item.position show_displayable_to_players(displayable) return end # What if there's no Displayable for this item? That might be okay or might not. return if item.is_a?(::Demiurge::InertStateItem) # Nothing needed for InertStateItems STDERR.puts "Don't know how to register or display this item: #{item.name.inspect}" end |
#remove_player(player) ⇒ Object
The logout action happens before this does, which may affect what's where.
453 454 455 |
# File 'lib/pixiurge/engine_connector.rb', line 453 def remove_player(player) @players.delete(player.name) end |
#set_player_backdrop(player, player_position, location_do) ⇒ Object
This is going to wind up as a complicated interaction at some point between the player and the zone/location. There need to be different areas where different levels of other-player activity are visible or invisible; that doesn't take things like sharding into account either...
416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 |
# File 'lib/pixiurge/engine_connector.rb', line 416 def set_player_backdrop(player, player_position, location_do) loc_name = location_do.name # Show the location's sprites player.destroy_all_displayables player.show_displayable(location_do) x, y = ::Demiurge::TiledLocation.position_to_coords(player_position) x ||= 0 y ||= 0 player.send_instant_pan_to_pixel_offset location_do.block_width * x, location_do.block_height * y # Anybody or anything else there? Show them to this player. each_displayable_for_location_name(location_do.name) do |displayable| player.show_displayable(displayable) end end |
#show_displayable_to_players(displayable, options = { :except => []}) ⇒ Object
390 391 392 393 394 395 396 |
# File 'lib/pixiurge/engine_connector.rb', line 390 def show_displayable_to_players(displayable, = { :except => []}) return unless displayable.position # Agents and some other items are allowed to have no position and just be instantiable loc_name = displayable.position.split("#")[0] each_player_for_location_name(loc_name, ) do |player| player.show_displayable(displayable) end end |
#start_simulation(options = {}) ⇒ void
This method returns an undefined value.
Make sure the simulation is running. Supply parameters about how exactly that should happen. You can pass in a constructed engine, or parameters for how to create one.
You should pass in up to one of :engine, :engine_text, :engine_files or :engine_dsl_dir to create the Engine or use a created Engine. If you don't pass one, Pixiurge will assume an :engine_dsl_dir of "world" under the App#root_dir.
A DSL directory, if one is included, will assume that any Ruby file under an extensions directory or subdirectory will be loaded directly as Ruby code. Any Ruby file not under an extensions directory or subdirectory will be loaded as Demiurge World File DSL.
By default the Demiurge Engine will run at 2 ticks per second, or 500 milliseconds per tick. You can alter this with the :ms_per_tick option.
By default, Pixiurge will save state automatically every 600 ticks into a timestamped file in the "state" subdirectory. The :autosave_ticks option can be set to a number of ticks for how often to save, or 0 for no autosave. You can set it to 0 and configure an autosave yourself by subscribing to the Demiurge::Notifications::TickFinished notification as well. The :autosave_path option says where to put the autosave file when it occurs. The substring "%TICKS%" will be replaced with the number of ticks completed if it is part of the :autosave_path.
108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 |
# File 'lib/pixiurge/engine_connector.rb', line 108 def start_simulation( = {}) raise("Simulation is already configured!") if @engine_configured || @engine_started = .keys - SIMULATION_OPTIONS raise "Passed illegal options to #start_simulation! #{.inspect}" unless .empty? if [:engine] engine = [:engine] elsif [:engine_text] engine = Demiurge::DSL.engine_from_dsl_text [:engine_text] elsif [:engine_files] engine = Demiurge::DSL.engine_from_dsl_files [:engine_files] else dsl_dir = [:engine_dsl_dir] || File.join(@root_dir, "world") # Require all Ruby extensions under the World dir ruby_extensions = Dir["#{dsl_dir}/**/extensions/**/*.rb"] ruby_extensions.each { |filename| require filename } # Load all Worldfile non-Ruby-extension files as World File DSL dsl_files = Dir["#{dsl_dir}/**/*.rb"] - ruby_extensions engine = Demiurge::DSL.engine_from_dsl_files *dsl_files end @ms_per_tick = [:ms_per_tick] || 500 @autosave_ticks = [:autosave_ticks] || 600 @autosave_path = [:autosave_path] || "state/autosave_%TICKS%.json" # Subscribe to notifications and sync up with all existing engine # objects before we actually start the simulation. hook_up_engine(engine) if [:engine_restore_statefile] last_statefile = [:engine_restore_statefile] puts "Restoring state data from #{last_statefile.inspect}." state_data = MultiJson.load File.read(last_statefile) @engine.load_state_from_dump(state_data) end @engine_configured = true #start_engine_periodic_timer end |
#thin_eventmachine_loop(rack_app, port, ssl_key_path, ssl_cert_path) ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Start the EventMachine loop to run Thin, the periodic timer and so on.
192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 |
# File 'lib/pixiurge/engine_connector.rb', line 192 def thin_eventmachine_loop(rack_app, port, ssl_key_path, ssl_cert_path) # No luck with Puma - for now, hardcode using Thin Faye::WebSocket.load_adapter('thin') EventMachine.run { thin = Rack::Handler.get('thin') thin.run(rack_app, :Port => port) do |server| server. = { # Supported options: http://www.rubydoc.info/github/eventmachine/eventmachine/EventMachine/Connection:start_tls :private_key_file => ssl_key_path, :cert_chain_file => ssl_cert_path, :verify_peer => false, } server.ssl = true end Signal.trap("INT") { EventMachine.stop } Signal.trap("TERM") { EventMachine.stop } # And now, the purpose of this whole loop - allowing WebSockets, # the Thin server *and* a single periodic timer to all run at # once without multiple threads, and still have the timer start # when the server does. *sigh* start_engine_periodic_timer } STDERR.puts "Killed by SIGINT or SIGTERM... Exiting!" end |