Basic networking implemented, currently non functional

This commit is contained in:
2019-11-19 14:48:12 -06:00
parent b17842ab75
commit 82db9dd14d
14 changed files with 283 additions and 14 deletions

4
.gitignore vendored
View File

@@ -1 +1,3 @@
data/settings.json
data/settings.json
doc/
.yardoc/

View File

@@ -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

View File

@@ -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

View File

@@ -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

52
lib/networking/client.rb Normal file
View File

@@ -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

View File

@@ -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

42
lib/networking/packet.rb Normal file
View File

@@ -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

View File

@@ -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

51
lib/networking/server.rb Normal file
View File

@@ -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

View File

@@ -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

View File

View File

@@ -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

View File

@@ -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

View File

@@ -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