From 82db9dd14dbf4a722d0754f5b299724d0a5da060 Mon Sep 17 00:00:00 2001 From: Cyberarm Date: Tue, 19 Nov 2019 14:48:12 -0600 Subject: [PATCH] Basic networking implemented, currently non functional --- .gitignore | 4 +- i-mic-rts.rb | 7 ++++ lib/connection.rb | 59 +++++++++++++++++++++++++---- lib/director.rb | 6 ++- lib/networking/client.rb | 52 +++++++++++++++++++++++++ lib/networking/connection.rb | 25 ++++++++++++ lib/networking/packet.rb | 42 ++++++++++++++++++++ lib/networking/protocol.rb | 32 ++++++++++++++++ lib/networking/server.rb | 51 +++++++++++++++++++++++++ lib/player.rb | 2 +- lib/server/.gitkeep | 0 lib/states/game.rb | 5 ++- lib/states/menus/solo_lobby_menu.rb | 5 ++- lib/window.rb | 7 ++++ 14 files changed, 283 insertions(+), 14 deletions(-) create mode 100644 lib/networking/client.rb create mode 100644 lib/networking/connection.rb create mode 100644 lib/networking/packet.rb create mode 100644 lib/networking/protocol.rb create mode 100644 lib/networking/server.rb delete mode 100644 lib/server/.gitkeep diff --git a/.gitignore b/.gitignore index cb1d15f..4e18660 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,3 @@ -data/settings.json \ No newline at end of file +data/settings.json +doc/ +.yardoc/ \ No newline at end of file diff --git a/i-mic-rts.rb b/i-mic-rts.rb index 71278db..f2e95f6 100755 --- a/i-mic-rts.rb +++ b/i-mic-rts.rb @@ -9,6 +9,7 @@ end require "nokogiri" require "json" +require "socket" require_relative "lib/version" require_relative "lib/errors" @@ -39,6 +40,12 @@ require_relative "lib/director" require_relative "lib/player" require_relative "lib/connection" +require_relative "lib/networking/protocol" +require_relative "lib/networking/packet" +require_relative "lib/networking/server" +require_relative "lib/networking/client" +require_relative "lib/networking/connection" + IMICRTS::Setting.setup IMICRTS::Window.new(width: Gosu.screen_width / 4 * 3, height: Gosu.screen_height / 4 * 3, fullscreen: false, resizable: true).show \ No newline at end of file diff --git a/lib/connection.rb b/lib/connection.rb index 44f9f8b..5cbf151 100644 --- a/lib/connection.rb +++ b/lib/connection.rb @@ -1,7 +1,30 @@ class IMICRTS + # {IMICRTS::Connection} is the abstract middleman that Director sends/receives Orders from. + # not to be confused with {IMICRTS::Networking::Connection} class Connection - def initialize(*args) + # Connection modes: + # :virtual => emulates networking without sockets (used for solo play) + # :host => starts server + # :client => connects to server/host + def initialize(director:, mode:, hostname: "localhost", port: 56789) + @director = director + @mode = mode + @hostname = hostname + @port = port + @pending_orders = [] + + case @mode + when :virtual + when :host + @server = Networking::Server.new(director: @director, hostname: @hostname, port: @port) + + @connection = Networking::Connection.new(director: @director, hostname: @hostname, port: @port) + when :client + @connection = Networking::Connection.new(director: @director, hostname: @hostname, port: @port) + else + raise RuntimeError, "Unable to process Connection of type: #{@mode.inspect}" + end end def add_order(order) @@ -9,15 +32,37 @@ class IMICRTS end def update - data = @pending_orders.sort_by { |order| order.tick_id }.map do |order| + # data = @pending_orders.sort_by { |order| order.tick_id }.map do |order| - # Order serialized size in bytes + serialized order data - [order.serialized_order.length].pack("n") + order.serialized_order - end.join + # # Order serialized size in bytes + serialized order data + # pp order.serialized_order + # [order.serialized_order.length].pack("n") + order.serialized_order + # end.join - # p data if data.length > 0 + if @mode == :virtual + else + @pending_orders.each do |order| + # TODO: make this include order_id and tick_id + @server.broadcast(order.serialized_order) if @server + @connection.write(order.serialized_order) unless @server - @pending_orders.clear + @pending_orders.delete(order) + end + end + + case @mode + when :virtual + when :host + @server.update + @connection.update + when :client + @connection.update + end + end + + def finalize + @server.stop if @server + @connection.close if @connection end end end \ No newline at end of file diff --git a/lib/director.rb b/lib/director.rb index c9afeff..9bd7eee 100644 --- a/lib/director.rb +++ b/lib/director.rb @@ -1,7 +1,7 @@ class IMICRTS class Director attr_reader :current_tick, :map - def initialize(map:, players:, networking_mode: :virtual, tick_rate: 10) + def initialize(map:, players:, networking_mode:, tick_rate: 10) @map = map @players = players @connection = IMICRTS::Connection.new(director: self, mode: networking_mode) @@ -104,5 +104,9 @@ class IMICRTS def entities @players.map { |player| player.entities }.flatten end + + def finalize + @connection.finalize + end end end \ No newline at end of file diff --git a/lib/networking/client.rb b/lib/networking/client.rb new file mode 100644 index 0000000..1b4aded --- /dev/null +++ b/lib/networking/client.rb @@ -0,0 +1,52 @@ +class IMICRTS + class Networking + class Client + attr_reader :packets_sent, :packets_received, + :data_sent, :data_received + def initialize(socket) + @socket = socket + + @packets_sent, @packets_received = 0, 0 + @data_sent, @data_received = 0, 0 + + @read_queue = [] + @write_queue = [] + end + + def update + if connected? + buffer = @socket.recv_nonblock(Networking::Protocol.max_packet_length, exception: false) + + if buffer.is_a?(String) + order = buffer.split(Protocol::END_OF_MESSAGE).first.strip + end + + until(@write_queue.size == 0) + packet = @write_queue.shift + + @socket.write_nonblock(packet + Protocol::END_OF_MESSAGE, exception: false) + end + end + end + + def connected? + !@socket.closed? + end + + def close(packet = nil) + @socket.write(Networking::Packet.pack(packet) + Protocol::END_OF_MESSAGE) if packet + + @socket.close + end + + def write(data) + packet = Networking::Packet.new(type: Protocol::RELIABLE, client_id: 0, data: data) + @write_queue << Networking::Packet.pack(packet) + end + + def read + return @read_queue.shift + end + end + end +end \ No newline at end of file diff --git a/lib/networking/connection.rb b/lib/networking/connection.rb new file mode 100644 index 0000000..31c7a5f --- /dev/null +++ b/lib/networking/connection.rb @@ -0,0 +1,25 @@ +class IMICRTS + class Networking + class Connection + def initialize(director:, hostname:, port:) + @director = director + @hostname = hostname + @port = port + + @client = Networking::Client.new(TCPSocket.new(@hostname, @port)) + end + + def connected? + @client.connected? + end + + def update + @client.update + end + + def close + @client.close + end + end + end +end \ No newline at end of file diff --git a/lib/networking/packet.rb b/lib/networking/packet.rb new file mode 100644 index 0000000..b5276f4 --- /dev/null +++ b/lib/networking/packet.rb @@ -0,0 +1,42 @@ +class IMICRTS + class Networking + class Packet + # Packet + # [ + # header_packet_type, + # header_packet_length, + # header_packet_sequence_id, + # header_packet_client_id, + # + # data + # ] + attr_reader :type, :sequence_id, :client_id, :data + def initialize(type:, sequence_id: nil, client_id:, data:) + @type = type + @sequence_id = sequence_id + @client_id = client_id + @data = data + end + + def self.pack(packet) + header = nil + + # Packet Type: Char => "C" + # Packet Sequence ID: 32-bit unsigned Integer => "N" + # Packet Client ID: 16-bit unsigned Integer => "n" + + if packet.sequence_id + header = [packet.type, packet.sequence_id, packet.client_id].pack("CNn") + else + header = [packet.type, packet.client_id].pack("Cn") + end + + header += packet.data + end + + def self.unpack(raw_string) + pp raw_string.unpack("Cn") + end + end + end +end \ No newline at end of file diff --git a/lib/networking/protocol.rb b/lib/networking/protocol.rb new file mode 100644 index 0000000..fdf2568 --- /dev/null +++ b/lib/networking/protocol.rb @@ -0,0 +1,32 @@ +class IMICRTS + class Networking + module Protocol + VERSION = 0x01 + END_OF_MESSAGE = "\r\r\r\n" + + ACKNOWLEDGE = 0x01 + CONNECT = 0x02 + VERIFY_CONNECT = 0x03 + DISCONNECT = 0x04 + PING = 0x05 + + RELIABLE = 0x20 + UNRELIABLE = 0x21 + UNSEQUENCED = 0x22 + FRAGMENT = 0x23 + UNRELIABLE_FRAGMENT = 0x24 + + BANDWIDTH_LIMIT = 0x40 + THROTTLE_CONFIGURE = 0x41 + COUNT = 0x42 + MASK = 0x43 + + HEADER_FLAG_COMPRESSED = 0x00 + HEADER_FLAG_SENT_TIME = 0x01 + + def self.max_packet_length + 1024 # => 1 Kb + end + end + end +end \ No newline at end of file diff --git a/lib/networking/server.rb b/lib/networking/server.rb new file mode 100644 index 0000000..5ff3573 --- /dev/null +++ b/lib/networking/server.rb @@ -0,0 +1,51 @@ +class IMICRTS + class Networking + class Server + def initialize(director:, hostname:, port:, max_peers: 8) + @director = director + @hostname, @port = hostname, port + @max_peers = max_peers + + @socket = TCPServer.new(hostname, port) + @clients = [] + end + + def update + new_client = @socket.accept_nonblock(exception: false) + + if new_client != :wait_readable + handle_client(new_client) + end + + @clients.each do |client| + client.update + end + end + + def handle_client(client) + if @clients.size < @max_peers + @clients << Networking::Client.new(client) + else + client.write("\u00000128") + client.close + end + end + + def broadcast(packet) + @clients.each do |client| + client.write(packet) + end + end + + def write(client_id, packet) + client = @clients.find { |cl| cl.uid == client_id } + + client.write(packet) if client + end + + def stop + @socket.close if @socket + end + end + end +end \ No newline at end of file diff --git a/lib/player.rb b/lib/player.rb index 1848b4d..a24b27a 100644 --- a/lib/player.rb +++ b/lib/player.rb @@ -33,7 +33,7 @@ class IMICRTS end class ScheduledOrder - attr_reader :tick_id, :serialized_order + attr_reader :order_id, :tick_id, :serialized_order def initialize(order_id, tick_id, serialized_order) @order_id = order_id @tick_id, @serialized_order = tick_id, serialized_order diff --git a/lib/server/.gitkeep b/lib/server/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/lib/states/game.rb b/lib/states/game.rb index 916875c..a8daddd 100644 --- a/lib/states/game.rb +++ b/lib/states/game.rb @@ -3,9 +3,10 @@ class IMICRTS Overlay = Struct.new(:image, :position, :angle, :alpha) def setup window.show_cursor = true + @options[:networking_mode] ||= :host @player = Player.new(id: 0) - @director = Director.new(map: Map.new(map_file: "maps/test_map.tmx"), players: [@player]) + @director = Director.new(map: Map.new(map_file: "maps/test_map.tmx"), networking_mode: @options[:networking_mode], players: [@player]) @selected_entities = [] @overlays = [] @@ -178,7 +179,7 @@ class IMICRTS end def finalize - # TODO: Release bound objects/remove self from Window.states array + @director.finalize end end end \ No newline at end of file diff --git a/lib/states/menus/solo_lobby_menu.rb b/lib/states/menus/solo_lobby_menu.rb index 7118526..e28f09f 100644 --- a/lib/states/menus/solo_lobby_menu.rb +++ b/lib/states/menus/solo_lobby_menu.rb @@ -18,7 +18,7 @@ class IMICRTS stack do case elements[index] when :edit_line - edit_line Setting.get(:player_name) + @player_name = edit_line Setting.get(:player_name) when :button button item when :toggle_button @@ -35,7 +35,8 @@ class IMICRTS flow(width: 1.0) do button("Accept", width: 0.5) do - push_state(Game) + Setting.set(:player_name, @player_name.value) + push_state(Game, networking_mode: :virtual) end button("Back", align: :right) do diff --git a/lib/window.rb b/lib/window.rb index 22a8f8d..f0b502c 100644 --- a/lib/window.rb +++ b/lib/window.rb @@ -44,5 +44,12 @@ class IMICRTS def dt delta_time / 1000.0 end + + # Override CyberarmEngine::Window#push_state to only ever have 1 state + def push_state(*args) + @states.clear + + super(*args) + end end end \ No newline at end of file