mirror of
https://github.com/cyberarm/i-mic-fps.git
synced 2025-12-15 15:42:35 +00:00
More work on implementing networking
This commit is contained in:
@@ -1,26 +1,32 @@
|
|||||||
class IMICFPS
|
class IMICFPS
|
||||||
class NetworkManager
|
module Networking
|
||||||
MULTICAST_ADDRESS = "224.0.0.1"
|
MULTICAST_ADDRESS = "224.0.0.1"
|
||||||
MULTICAST_PORT = 30_000
|
MULTICAST_PORT = 30_000
|
||||||
|
|
||||||
REMOTE_GAMEHUB = "i-mic.rubyclan.org"
|
REMOTE_GAMEHUB = "i-mic.cyberarm.dev"
|
||||||
REMOTE_GAMEHUB_PORT = 98765
|
REMOTE_GAMEHUB_PORT = 98765
|
||||||
|
|
||||||
DEFAULT_SERVER_HOST = "0.0.0.0"
|
DEFAULT_SERVER_HOST = "0.0.0.0"
|
||||||
DEFAULT_SERVER_PORT = 56789
|
DEFAULT_SERVER_PORT = 56789
|
||||||
DEFAULT_SERVER_QUERY_PORT = 28900
|
DEFAULT_SERVER_QUERY_PORT = 28900
|
||||||
def initialize
|
|
||||||
|
RESERVED_PEER_ID = 0
|
||||||
|
DEFAULT_PEER_LIMIT = 32
|
||||||
|
HARD_PEER_LIMIT = 254
|
||||||
|
|
||||||
|
def self.milliseconds
|
||||||
|
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
||||||
end
|
end
|
||||||
|
|
||||||
# https://github.com/jpignata/blog/blob/master/articles/multicast-in-ruby.md
|
# https://github.com/jpignata/blog/blob/master/articles/multicast-in-ruby.md
|
||||||
def broadcast_lan_lobby
|
def self.broadcast_lan_lobby
|
||||||
socket = UDPSocket.open
|
socket = UDPSocket.open
|
||||||
socket.setsockopt(:IPPROTO_IP, :IP_MULTICAST_TTL, 1)
|
socket.setsockopt(:IPPROTO_IP, :IP_MULTICAST_TTL, 1)
|
||||||
socket.send("IMICFPS_LAN_LOBBY", 0, MULTICAST_ADDRESS, MULTICAST_PORT)
|
socket.send("IMICFPS_LAN_LOBBY", 0, MULTICAST_ADDRESS, MULTICAST_PORT)
|
||||||
socket.close
|
socket.close
|
||||||
end
|
end
|
||||||
|
|
||||||
def handle_lan_multicast
|
def self.handle_lan_multicast
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
class IMICFPS
|
|
||||||
module Networking
|
|
||||||
class MemoryConnection < Connection
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
class IMICFPS
|
|
||||||
module Networking
|
|
||||||
class MemoryServer < Server
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
class IMICFPS
|
|
||||||
module Networking
|
|
||||||
class Client
|
|
||||||
attr_reader :packets_sent, :packets_received,
|
|
||||||
:data_sent, :data_received
|
|
||||||
def initialize(socket:)
|
|
||||||
@socket = socket
|
|
||||||
@write_queue = []
|
|
||||||
@read_queue = []
|
|
||||||
|
|
||||||
@packets_sent = 0
|
|
||||||
@packets_received = 0
|
|
||||||
@data_sent = 0
|
|
||||||
@data_received = 0
|
|
||||||
end
|
|
||||||
|
|
||||||
def read
|
|
||||||
data = @socket.recvfrom_nonblock(Protocol::MAX_PACKET_SIZE)
|
|
||||||
@read_queue << Packet.decode(data)
|
|
||||||
|
|
||||||
@packets_received += 1
|
|
||||||
@data_received += data.length
|
|
||||||
rescue IO::WaitReadable
|
|
||||||
end
|
|
||||||
|
|
||||||
def write
|
|
||||||
@write_queue.each do |packet|
|
|
||||||
raw = Packet.encode
|
|
||||||
@socket.send(raw, 0)
|
|
||||||
@write_queue.delete(packet)
|
|
||||||
|
|
||||||
@packets_sent += 1
|
|
||||||
@data_sent += raw.length
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def puts(packet)
|
|
||||||
@write_queue << packet
|
|
||||||
end
|
|
||||||
|
|
||||||
def gets
|
|
||||||
@read_queue.shift
|
|
||||||
end
|
|
||||||
|
|
||||||
def close
|
|
||||||
@socket.close if @socket
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -1,16 +1,75 @@
|
|||||||
class IMICFPS
|
class IMICFPS
|
||||||
module Networking
|
module Networking
|
||||||
class Connection
|
class Connection
|
||||||
def initialize(hostname:, port:)
|
attr_reader :address, :port
|
||||||
|
attr_accessor :total_packets_sent, :total_packets_received, :total_data_sent, :total_data_received, :last_read_time, :last_write_time
|
||||||
|
def initialize(address:, port:)
|
||||||
|
@address = address
|
||||||
|
@port = port
|
||||||
|
|
||||||
|
|
||||||
|
@read_buffer = ReadBuffer.new
|
||||||
|
@write_queue = []
|
||||||
|
|
||||||
|
@last_read_time = Networking.milliseconds
|
||||||
|
@last_write_time = Networking.milliseconds
|
||||||
|
|
||||||
|
@total_packets_sent = 0
|
||||||
|
@total_packets_received = 0
|
||||||
|
@total_data_sent = 0
|
||||||
|
@total_data_received = 0
|
||||||
|
|
||||||
|
@socket = nil
|
||||||
end
|
end
|
||||||
|
|
||||||
def connect
|
def connect
|
||||||
|
@socket = UDPSocket.new
|
||||||
|
@socket.connect(@address, @port)
|
||||||
|
|
||||||
|
send_packet(
|
||||||
|
Packet.new(
|
||||||
|
peer_id: 0,
|
||||||
|
sequence: 0,
|
||||||
|
type: Protocol::CONNECT,
|
||||||
|
payload: "Hello World!"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def send_packet( packet )
|
||||||
end
|
end
|
||||||
|
|
||||||
def update
|
def update
|
||||||
|
while(read)
|
||||||
|
end
|
||||||
|
|
||||||
|
write
|
||||||
|
|
||||||
|
@read_buffer.reconstruct_packets.each do |packet|
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def close
|
def close
|
||||||
|
@socket.close if @socket
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
def read
|
||||||
|
data, addr = @socket.recvfrom_nonblock(Protocol::MAX_PACKET_SIZE)
|
||||||
|
@read_buffer.add(data, addr )
|
||||||
|
|
||||||
|
@total_packets_received += 1
|
||||||
|
@total_data_received += data.length
|
||||||
|
@last_read_time = Networking.milliseconds
|
||||||
|
return true
|
||||||
|
rescue IO::WaitReadable
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
def write
|
||||||
|
while(packet = @write_queue.shift)
|
||||||
|
@socket.send( packet.encode, 0, @address, @port )
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,29 +1,24 @@
|
|||||||
class IMICFPS
|
class IMICFPS
|
||||||
module Networking
|
module Networking
|
||||||
class Director
|
class Director
|
||||||
attr_reader :mode, :hostname, :port, :tick_rate, :storage
|
attr_reader :address, :port, :tick_rate, :storage, :map
|
||||||
def initialize(mode:, hostname:, port:, interface:, state: nil, tick_rate: 2)
|
def initialize(address: DEFAULT_SERVER_HOST, port: DEFAULT_SERVER_PORT, tick_rate: 2)
|
||||||
@mode = mode
|
@address = address
|
||||||
@hostname = hostname
|
|
||||||
@port = port
|
@port = port
|
||||||
@state = state
|
|
||||||
@tick_rate = (1000.0 / tick_rate) / 1000.0
|
@tick_rate = (1000.0 / tick_rate) / 1000.0
|
||||||
|
|
||||||
case @mode
|
@server = Server.new(address: @address, port: @port, max_peers: DEFAULT_PEER_LIMIT)
|
||||||
when :server
|
@server.bind
|
||||||
@server = interface.new(hostname: @hostname, port: @port)
|
|
||||||
when :connection
|
|
||||||
@connection = interface.new(hostname: @hostname, port: @port)
|
|
||||||
when :memory
|
|
||||||
@server = interface[:server].new(hostname: @hostname, port: @port)
|
|
||||||
@connection = interface[:connection].new(hostname: @hostname, port: @port)
|
|
||||||
else
|
|
||||||
raise ArgumentError, "Expected mode to be :server, :connection, or :memory, not #{mode.inspect}"
|
|
||||||
end
|
|
||||||
|
|
||||||
@last_tick_time = milliseconds
|
@last_tick_time = Networking.milliseconds
|
||||||
@directing = true
|
@directing = true
|
||||||
@storage = {}
|
@storage = {}
|
||||||
|
@map = nil
|
||||||
|
end
|
||||||
|
|
||||||
|
def load_map(map_parser:)
|
||||||
|
# TODO: send map_change to clients
|
||||||
|
@map = Map.new(map_parser: map_parser)
|
||||||
end
|
end
|
||||||
|
|
||||||
def run
|
def run
|
||||||
@@ -33,28 +28,24 @@ class IMICFPS
|
|||||||
|
|
||||||
tick(dt)
|
tick(dt)
|
||||||
|
|
||||||
@server.update if @server
|
|
||||||
@connection.update if @connection
|
|
||||||
|
|
||||||
@last_tick_time = milliseconds
|
@last_tick_time = milliseconds
|
||||||
sleep(@tick_rate)
|
sleep(@tick_rate)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def tick(dt)
|
def tick(delta_time)
|
||||||
|
if @map
|
||||||
|
Publisher.instance.publish(:tick, delta_time * 1000.0)
|
||||||
|
|
||||||
|
@map.update
|
||||||
|
@server.update
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def shutdown
|
def shutdown
|
||||||
@directing = false
|
@directing = false
|
||||||
|
@server.close
|
||||||
@clients.each(&:close)
|
|
||||||
@server.update if @server
|
|
||||||
@connection.update if @connection
|
|
||||||
end
|
|
||||||
|
|
||||||
def milliseconds
|
|
||||||
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,22 +1,32 @@
|
|||||||
class IMICFPS
|
class IMICFPS
|
||||||
module Networking
|
module Networking
|
||||||
class Packet
|
class Packet
|
||||||
HEADER_PACKER = "CnCnC"
|
HEADER_PACKER = "CnCnCC"
|
||||||
HEADER_SIZE = 7
|
HEADER_SIZE = 8
|
||||||
|
|
||||||
def self.from_stream(raw)
|
def self.from_stream(raw)
|
||||||
header = raw[ 0..HEADER_SIZE ].unpack(HEADER_PACKER)
|
header = raw[ 0..HEADER_SIZE ].unpack(HEADER_PACKER)
|
||||||
payload = raw[HEADER_SIZE + 1..raw.length - 1]
|
payload = raw[HEADER_SIZE..raw.length - 1]
|
||||||
|
|
||||||
new(header[1], [2], payload)
|
new(peer_id: header.last, sequence: header[1], type: header[2], payload: payload)
|
||||||
end
|
end
|
||||||
|
|
||||||
def initialize(sequence:, type:, payload:)
|
# TODO: Handle splitting big packets into smaller ones
|
||||||
|
def self.splinter(packet)
|
||||||
|
packets = [packet]
|
||||||
|
|
||||||
|
return packets
|
||||||
|
end
|
||||||
|
|
||||||
|
attr_reader :peer_id, :sequence_number, :packet_type, :parity, :payload, :content_length
|
||||||
|
def initialize(peer_id:, sequence:, type:, payload:)
|
||||||
|
@peer_id = peer_id
|
||||||
@sequence_number = sequence
|
@sequence_number = sequence
|
||||||
@packet_type = type
|
@packet_type = type
|
||||||
@content_length = payload.length
|
|
||||||
@parity = calculate_parity
|
@parity = calculate_parity
|
||||||
@payload = payload
|
@payload = payload
|
||||||
|
|
||||||
|
@content_length = payload.length
|
||||||
end
|
end
|
||||||
|
|
||||||
def header
|
def header
|
||||||
@@ -26,7 +36,8 @@ class IMICFPS
|
|||||||
@packet_type, # char
|
@packet_type, # char
|
||||||
@content_length, # uint16
|
@content_length, # uint16
|
||||||
@parity, # char
|
@parity, # char
|
||||||
].unpack(HEADER_PACKER)
|
@peer_id, # char
|
||||||
|
].pack(HEADER_PACKER)
|
||||||
end
|
end
|
||||||
|
|
||||||
def calculate_parity
|
def calculate_parity
|
||||||
@@ -38,6 +49,7 @@ class IMICFPS
|
|||||||
end
|
end
|
||||||
|
|
||||||
def decode(payload)
|
def decode(payload)
|
||||||
|
payload
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
23
lib/networking/peer.rb
Normal file
23
lib/networking/peer.rb
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
class IMICFPS
|
||||||
|
module Networking
|
||||||
|
class Peer
|
||||||
|
attr_reader :packet_read_queue, :packet_write_queue
|
||||||
|
attr_accessor :packets_sent, :packets_received, :data_sent, :data_received, :last_read_time, :last_write_time
|
||||||
|
def initialize(peer_id:, address:, port:)
|
||||||
|
@address, @port = address, port
|
||||||
|
@peer_id = peer_id
|
||||||
|
|
||||||
|
@packet_write_queue = []
|
||||||
|
@packet_read_queue = []
|
||||||
|
|
||||||
|
@last_read_time = Networking.milliseconds
|
||||||
|
@last_write_time = Networking.milliseconds
|
||||||
|
|
||||||
|
@packets_sent = 0
|
||||||
|
@packets_received = 0
|
||||||
|
@data_sent = 0
|
||||||
|
@data_received = 0
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
30
lib/networking/read_buffer.rb
Normal file
30
lib/networking/read_buffer.rb
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
class IMICFPS
|
||||||
|
module Networking
|
||||||
|
class ReadBuffer
|
||||||
|
def initialize
|
||||||
|
@buffer = []
|
||||||
|
end
|
||||||
|
|
||||||
|
def add(buffer, addr_info)
|
||||||
|
@buffer << { buffer: buffer, addr_info: addr_info }
|
||||||
|
end
|
||||||
|
|
||||||
|
def reconstruct_packets
|
||||||
|
packets = []
|
||||||
|
|
||||||
|
@buffer.each do |buffer, addr_info|
|
||||||
|
packet = Packet.from_stream(buffer)
|
||||||
|
|
||||||
|
if packet.valid?
|
||||||
|
@buffer.delete(buffer)
|
||||||
|
else
|
||||||
|
puts "Invalid packet: #{packet}"
|
||||||
|
@buffer.delete(buffer)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return packets
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -1,28 +1,102 @@
|
|||||||
class IMICFPS
|
class IMICFPS
|
||||||
module Networking
|
module Networking
|
||||||
class Server
|
class Server
|
||||||
MAX_CLIENTS = 32
|
attr_reader :address, :port, :max_peers, :peers
|
||||||
|
attr_accessor :total_packets_sent, :total_packets_received, :total_data_sent, :total_data_received, :last_read_time, :last_write_time
|
||||||
attr_reader :hostname, :port, :max_clients, :clients
|
def initialize(address:, port:, max_peers:)
|
||||||
def initialize(hostname:, port:, max_clients: MAX_CLIENTS)
|
@address = address
|
||||||
@hostname = hostname
|
|
||||||
@port = port
|
@port = port
|
||||||
@max_clients = max_clients
|
@max_peers = max_peers
|
||||||
|
|
||||||
@clients = []
|
@peers = Array.new(@max_peers + 1, nil)
|
||||||
@socket = nil
|
|
||||||
|
@read_buffer = ReadBuffer.new
|
||||||
|
|
||||||
|
@last_read_time = Networking.milliseconds
|
||||||
|
@last_write_time = Networking.milliseconds
|
||||||
|
|
||||||
|
@total_packets_sent = 0
|
||||||
|
@total_packets_received = 0
|
||||||
|
@total_data_sent = 0
|
||||||
|
@total_data_received = 0
|
||||||
|
end
|
||||||
|
|
||||||
|
# Peer ID 0 is reserved for unconnected peers to pass packet validation
|
||||||
|
def create_peer(address:, port:)
|
||||||
|
@peers.each_with_index do |peer, i|
|
||||||
|
unless peer
|
||||||
|
new_peer = Peer.new(peer_id: i + 1, address: address, port: port)
|
||||||
|
@peers[i + 1] = new_peer
|
||||||
|
|
||||||
|
return new_peer
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def get_peer(peer_id)
|
||||||
|
@peers[peer_id + 1]
|
||||||
|
end
|
||||||
|
|
||||||
|
def remove_peer(peer_id)
|
||||||
|
@peers[peer_id] = nil
|
||||||
end
|
end
|
||||||
|
|
||||||
def bind
|
def bind
|
||||||
|
@socket = UDPSocket.new
|
||||||
|
@socket.bind(@address, @port)
|
||||||
end
|
end
|
||||||
|
|
||||||
def broadcast(packet)
|
def send_packet( peer_id, packet )
|
||||||
|
if peer = get_peer(peer_id)
|
||||||
|
packets = Packet.splinter(packet)
|
||||||
|
|
||||||
|
packets.each { |pkt| peer.write_queue.add(pkt) }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def broadcast_packet(packet)
|
||||||
|
@peers.each do |peer|
|
||||||
|
send_packet(peer.peer_id, packet)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def update
|
def update
|
||||||
|
while(read)
|
||||||
|
end
|
||||||
|
|
||||||
|
# "deliver" packets to peers, record stats to peers
|
||||||
|
@read_buffer.reconstruct_packets.each do |packet|
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def close
|
def close
|
||||||
|
@socket.close if @socket
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
def read
|
||||||
|
data, addr = @socket.recvfrom_nonblock(Protocol::MAX_PACKET_SIZE)
|
||||||
|
@read_buffer.add(data, addr )
|
||||||
|
|
||||||
|
@total_packets_received += 1
|
||||||
|
@total_data_received += data.length
|
||||||
|
@last_read_time = Networking.milliseconds
|
||||||
|
return true
|
||||||
|
rescue IO::WaitReadable
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
def write(peer, packet)
|
||||||
|
raw = packet.encode
|
||||||
|
@socket.send( raw, 0, peer.address, peer.port )
|
||||||
|
|
||||||
|
@total_packets_sent += 1
|
||||||
|
@total_data_sent += raw.length
|
||||||
|
@last_write_time = Networking.milliseconds
|
||||||
|
|
||||||
|
peer.total_packets_sent += 1
|
||||||
|
peer.total_data_sent += raw.length
|
||||||
|
peer.last_write_time = Networking.milliseconds
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -9,7 +9,11 @@ class IMICFPS
|
|||||||
@player = @map.find_entity_by(name: "character")
|
@player = @map.find_entity_by(name: "character")
|
||||||
@camera = Camera.new(position: @player.position.clone)
|
@camera = Camera.new(position: @player.position.clone)
|
||||||
@camera.attach_to(@player)
|
@camera.attach_to(@player)
|
||||||
@director = Networking::Director.new(mode: :memory, hostname: "i-mic.rubyclan.org", port: 56789, interface: { server: Networking::MemoryServer, connection: Networking::MemoryConnection }, state: self)
|
@director = Networking::Director.new
|
||||||
|
@director.load_map(map_parser: @options[:map_parser])
|
||||||
|
|
||||||
|
@connection = Networking::Connection.new(address: "localhost", port: Networking::DEFAULT_SERVER_PORT)
|
||||||
|
@connection.connect
|
||||||
|
|
||||||
@crosshair = Crosshair.new
|
@crosshair = Crosshair.new
|
||||||
@hud = HUD.new(@player)
|
@hud = HUD.new(@player)
|
||||||
@@ -35,15 +39,13 @@ class IMICFPS
|
|||||||
def update
|
def update
|
||||||
update_text
|
update_text
|
||||||
|
|
||||||
Publisher.instance.publish(:tick, Gosu.milliseconds - window.delta_time)
|
|
||||||
|
|
||||||
@map.update
|
|
||||||
|
|
||||||
control_player
|
control_player
|
||||||
@hud.update
|
@hud.update
|
||||||
|
|
||||||
@camera.update
|
@camera.update
|
||||||
|
@connection.update
|
||||||
@director.tick(window.dt)
|
@director.tick(window.dt)
|
||||||
|
@map.update
|
||||||
|
|
||||||
if window.config.get(:debug_options, :stats)
|
if window.config.get(:debug_options, :stats)
|
||||||
@text.text = update_text
|
@text.text = update_text
|
||||||
|
|||||||
Reference in New Issue
Block a user