Added basic dialogs, added 'blindman' implementation of TACNET networking code, added font

This commit is contained in:
2020-06-07 15:18:31 -05:00
parent 5ea8d13653
commit c694d29050
20 changed files with 870 additions and 5 deletions

49
lib/backend.rb Normal file
View File

@@ -0,0 +1,49 @@
module TAC
class Backend
attr_reader :config, :tacnet
def initialize
@config = load_config
@tacnet = TACNET.new
end
def load_config
if File.exist?(TAC::CONFIG_PATH)
JSON.parse(File.read( TAC::CONFIG_PATH ))
else
write_default_config
load_config
end
end
def write_default_config
File.open(TAC::CONFIG_PATH, "w") do |f|
f.write JSON.dump(
{
config: {
spec_version: TAC::CONFIG_SPEC_VERSION,
hostname: TACNET::DEFAULT_HOSTNAME,
port: TACNET::DEFAULT_PORT,
presets: [],
},
data: {
groups: [],
actions: [],
values: [],
},
}
)
end
end
def refresh_config
load_config
$window.states.clear
$window.push_state(Editor)
end
def refresh_tacnet_status
$window.current_state.refresh_tacnet_status
end
end
end

66
lib/dialog.rb Normal file
View File

@@ -0,0 +1,66 @@
module TAC
class Dialog < CyberarmEngine::GuiState
def setup
theme(THEME)
background Gosu::Color.new(0x88_000000)
@title = @options[:title] ? @options[:title] : "#{self.class}"
@window_width, @window_height = window.width, window.height
@dialog_root = stack width: 250, height: 400, border_thickness: 2, border_color: [TAC::Palette::TIMECRAFTERS_PRIMARY, TAC::Palette::TIMECRAFTERS_SECONDARY] do
# Title bar
flow width: 1.0, height: 0.1 do
background [TAC::Palette::TIMECRAFTERS_PRIMARY, TAC::Palette::TIMECRAFTERS_SECONDARY]
# title
flow width: 0.9 do
label @title
end
# Buttons
flow width: 0.1 do
button "X", text_size: 24 do
close
end
end
end
# Dialog body
stack width: 1.0, height: 0.9 do
build
end
end
center_dialog
end
def build
end
def center_dialog
@dialog_root.style.x = window.width / 2 - @dialog_root.style.width / 2
@dialog_root.style.y = window.height / 2 - @dialog_root.style.height / 2
end
def draw
$window.previous_state.draw
Gosu.flush
super
end
def update
super
if window.width != @window_width or window.height != @window_height
center_dialog
@window_width, @window_height = window.width, window.height
end
end
def close
$window.pop_state
end
end
end

View File

@@ -0,0 +1,25 @@
module TAC
class Dialog
class NamePromptDialog < Dialog
def build
background Gosu::Color::GRAY
label @options[:subtitle]
flow width: 1.0 do
label "Name", width: 0.25
edit_line "", width: 0.70
end
flow width: 1.0 do
button "Cancel", width: 0.475 do
close
end
button @options[:submit_label], width: 0.475 do
@options[:callback].call(self)
end
end
end
end
end
end

View File

@@ -6,6 +6,7 @@ module TAC
TIMECRAFTERS_PRIMARY = Gosu::Color.new(0xff008000)
TIMECRAFTERS_SECONDARY = Gosu::Color.new(0xff006000)
TIMECRAFTERS_TERTIARY = Gosu::Color.new(0xff00d000)
TACNET_PRIMARY = Gosu::Color.new(0xff000080)
TACNET_SECONDARY = Gosu::Color.new(0xff000060)

View File

@@ -2,6 +2,8 @@ module TAC
class States
class Editor < CyberarmEngine::GuiState
def setup
theme(THEME)
stack width: 1.0, height: 1.0 do
stack width: 1.0, height: 0.1 do
background [TAC::Palette::TIMECRAFTERS_PRIMARY, TAC::Palette::TIMECRAFTERS_SECONDARY]
@@ -11,21 +13,29 @@ module TAC
label TAC::NAME, color: Gosu::Color::BLACK, bold: true
flow do
[:add, :delete, :clone, :create, :simulate].each do |b|
button b.capitalize, text_size: 18
button "Add Group", text_size: 18 do
push_state(TAC::Dialog::NamePromptDialog, title: "Create Group", subtitle: "Add Group", submit_label: "Add", callback: proc {|instance| instance.close })
end
button "Add Action", text_size: 18 do
push_state(TAC::Dialog::NamePromptDialog, title: "Create Action", subtitle: "Add Action", submit_label: "Add", callback: proc {|instance| instance.close })
end
button "Add Value", text_size: 18 do
push_state(TAC::Dialog::NamePromptDialog, title: "Create Value", subtitle: "Add Value", submit_label: "Add", callback: proc {|instance| instance.close })
end
end
end
flow width: 0.299 do
stack width: 0.5 do
label "TACNET", color: TAC::Palette::TACNET_PRIMARY
@tacnet_ip_address = label "192.168.49.1", color: TAC::Palette::TACNET_SECONDARY
label "TACNET v#{TACNET::Packet::PROTOCOL_VERSION}", color: TAC::Palette::TACNET_PRIMARY
@tacnet_ip_address = label "#{TACNET::DEFAULT_HOSTNAME}:#{TACNET::DEFAULT_PORT}", color: TAC::Palette::TACNET_SECONDARY
end
stack width: 0.499 do
@tacnet_status = label "Connection Error", background: TAC::Palette::TACNET_CONNECTION_ERROR, text_size: 18, padding: 5, margin_top: 2
@tacnet_connection_button = button "Connect", text_size: 18
@tacnet_connection_button = button "Connect", text_size: 18 do
window.backend.tacnet.connect
end
end
end
end

6
lib/tac.rb Normal file
View File

@@ -0,0 +1,6 @@
module TAC
ROOT_PATH = File.expand_path("../..", __FILE__)
CONFIG_PATH = "#{ROOT_PATH}/data/config.json"
CONFIG_SPEC_VERSION = 2
end

22
lib/tacnet.rb Normal file
View File

@@ -0,0 +1,22 @@
module TAC
class TACNET
DEFAULT_HOSTNAME = "192.168.49.1"
DEFAULT_PORT = 8962
SYNC_INTERVAL = 250 # ms
HEARTBEAT_INTERVAL = 1_500 # ms
def initialize
@connection = nil
@server = nil
end
def connect(hostname = DEFAULT_HOSTNAME, port = DEFAULT_PORT, error_callback = proc {})
return if @connection && @connect.connected?
@connection = Connection.new(hostname, port)
puts "Connecting..."
@connection.connect(error_callback)
end
end
end

122
lib/tacnet/client.rb Normal file
View File

@@ -0,0 +1,122 @@
module TAC
class TACNET
class Client
CHUNK_SIZE = 4096
attr_reader :uuid, :read_queue, :write_queue, :socket,
:packets_sent, :packets_received,
:data_sent, :data_received
attr_accessor :sync_interval
def initialize
@uuid = SecureRandom.uuid
@read_queue = []
@write_queue = []
@sync_interval = 100
@packets_sent, @packets_received = 0, 0
@data_sent, @data_received = 0, 0
end
def socket=(socket)
@socket = socket
listen
end
def listen
Thread.new do
while connected?
# Read from socket
while message_in = read
if message_in.empty?
break
else
@read_queue << message_in
@packets_received += 1
@data_received += message_in.length
end
end
# Write to socket
while message_out = @write_queue.shift
write(message_out)
@packets_sent += 1
@data_sent += message_out.length
end
sleep @sync_interval / 1000.0
end
end
end
def sync(&block)
block.call
end
def handle_read_queue
message = gets
while message
puts(message)
message = gets
end
end
def connected?
!closed?
end
def bound?
@socket.bound? if @socket
end
def closed?
@socket.closed? if @socket
end
def write(message)
@socket.puts("#{message}\r\n\n")
end
def read
message = ""
begin
data = @socket.readpartial(CHUNK_SIZE)
message += message
end until message.end_with?("\r\n\n")
return message
end
def puts(message)
@write_queue << message
end
def gets
@read_queue.shift
end
def encode(message)
return message
end
def decode(blob)
return blob
end
def flush
@socket.flush if socket
end
def close(reason = nil)
write(reason) if reason
@socket.close if @socket
end
end
end
end

58
lib/tacnet/connection.rb Normal file
View File

@@ -0,0 +1,58 @@
module TAC
class TACNET
class Connection
def initialize(hostname = DEFAULT_HOSTNAME, port = DEFAULT_PORT)
@hostname = hostname
@port = port
@last_sync_time = 0
@sync_interval = SYNC_INTERVAL
@last_heartbeat_sent = 0
@heartbeat_interval = HEARTBEAT_INTERVAL
@connection_handler = proc do
handle_connection
end
end
def connect(error_callback)
return if @client
@client = Client.new
Thread.new do
begin
@client.socket = Socket.tcp(@hostname, @port, connect_timeout: 5)
while @client && @client.connected?
if Gosu.milliseconds > @last_sync_time + @sync_interval
@last_sync_time = Gosu.milliseconds
@client.sync(@connection_handler)
end
end
rescue => error
p error
error_callback.call(error)
end
end
end
def handle_connection
if @client && @client.connected?
message = @client.gets
PacketHandler.handle(message) if message
if Gosu.milliseconds > @last_heartbeat_sent + @heartbeat_interval
last_heartbeat_sent = Gosu.milliseconds
client.puts(PacketHandler.packet_heartbeat)
end
end
end
end
end
end

89
lib/tacnet/packet.rb Normal file
View File

@@ -0,0 +1,89 @@
module TAC
class TACNET
class Packet
PROTOCOL_VERSION = "0"
PROTOCOL_HEADER_SEPERATOR = "|"
PROTOCOL_HEARTBEAT = "heartbeat"
PACKET_TYPES = {
handshake: 0,
heartbeat: 1,
dump_config: 2,
add_group: 3,
update_group: 4,
delete_group: 5,
add_action: 6,
update_action: 7,
delete_action: 8,
add_variable: 9,
update_variable: 10,
delete_variable: 11,
}
def self.from_stream(message)
slice = message.split("|", 4)
if slice.size < 4
warn "Failed to split packet along first 4 " + PROTOCOL_HEADER_SEPERATOR + ". Raw return: " + Arrays.toString(slice)
return nil
end
if slice.first != PROTOCOL_VERSION
warn "Incompatible protocol version received, expected: " + PROTOCOL_VERSION + " got: " + slice.first
return nil
end
unless valid_packet_type?(Integer(slice[1]))
warn "Unknown packet type detected: #{slice[1]}"
return nil
end
version = slice[0]
type = PACKET_TYPES.key(Integer(slice[1]))
content_length = Integer(slice[2])
content = slice[3]
return Packet.new(version, type, content_length, body)
end
def self.create(packet_type, body)
Packet.new(PROTOCOL_VERSION, packet_type, body.length, body)
end
def self.valid_packet_type?(packet_type)
PACKET_TYPES.values.find { |t| t == packet_type }
end
attr_reader :version, :type, :content_length, :body
def initialize(version, type, content_length, body)
@version = version
@type = type
@content_length = content_length
@body = body
end
def encode_header
string = ""
string += PROTOCOL_VERSION
string += PROTOCOL_HEADER_SEPERATOR
string += packet_type
string += PROTOCOL_HEADER_SEPERATOR
string += content_length
string += PROTOCOL_HEADER_SEPERATOR
return string
end
def valid?
true
end
def to_s
"#{encode_header}#{body}"
end
end
end
end

View File

@@ -0,0 +1,65 @@
module TAC
class TACNET
class PacketHandler
def initialize(host_is_a_connection: false)
@host_is_a_connection = host_is_a_connection
end
def handle(message)
packet = Packet.from_stream(message)
if packet
hand_off(packet)
else
warn "Rejected raw packet: #{message}"
end
end
def hand_off(packet)
case packet.type
when :handshake
handle_handshake(packet)
when :heartbeat
handle_heartbeat(packet)
when :dump_config
handle_dump_config(packet)
else
warn "No hand off available for packet type: #{packet.type}"
end
end
def handle_handshake(packet)
end
def handle_heartbeat(packet)
end
def handle_dump_config(packet)
begin
hash = JSON.parse(packet.body)
if @host_is_a_connection
File.open("#{TAC::ROOT_PATH}/data/config.json", "w") { |f| f.write packet.body }
$window.backend.update_config
end
rescue JSON::ParserError
end
end
def self.packet_handshake(client_uuid)
Packet.create(Packet::PACKET_TYPES[:handshake], client_uuid)
end
def self.packet_heartbeat
Packet.create(Packet::PACKET_TYPES[:heartbeat], Packet::PROTOCOL_VERSION)
end
def self.packet_dump_config(string)
string = string.gsub("\n", " ")
Packet.create(Packet::PACKET_TYPES[:dump_config], string)
end
end
end
end

129
lib/tacnet/server.rb Normal file
View File

@@ -0,0 +1,129 @@
module TAC
class TACNET
class Server
attr_reader :active_client,
:packets_sent, :packets_received, :data_sent, :data_received,
:client_last_packets_sent, :client_last_packets_received, :client_last_data_sent, :client_last_data_received
def initialize(port = DEFAULT_PORT)
@port = port
@socket = nil
@active_client = nil
@connection_attempts = 0
@max_connection_attempts = 10
@packets_sent, @packets_received, @client_last_packets_sent, @client_last_packets_received = 0, 0, 0, 0
@data_sent, @data_received, @client_last_data_sent, @client_last_data_received = 0, 0, 0, 0
@last_sync_time = 0
@sync_interval = SYNC_INTERVAL
@last_heartbeat_sent = 0
@heartbeat_interval = HEARTBEAT_INTERVAL
@client_handler_proc = proc do
handle_client
end
@packet_handler = PacketHandler.new
end
def start
Thread.new do
while !@socket && @connection_attempts < @max_connection_attempts
begin
@socket = TCPServer.new(@port)
rescue => error
p error
@connection_attempts += 1
retry
end
end
while !@socket.closed?
begin
run_server
rescue => error
p error
@socket.close
end
end
end
end
def run_server
while !@socket.closed?
client = Client.new
client.sync_interval = @sync_interval
client.socket = @socket.accept
unless @active_client && @active_client.closed?
warn "Too many clients, already have one connected!"
client.close("Too many clients!")
else
@active_client = client
# TODO: Backup local config
# SEND CONFIG
config = File.read(TAC::CONFIG_PATH)
@active_client.puts(PacketHandler.packet_handshake(@active_client.uuid))
@active_client.puts(PacketHandler.packet_dump_config(config))
Thread.new do
while @active_client && @active_client.connected?
if Gosu.milliseconds > @last_sync_time + @sync_interval
@last_sync_time = Gosu.milliseconds
@active_client.sync(@client_handler_proc)
update_stats
end
end
update_stats
@active_client = nil
@client_last_packets_sent = 0
@client_last_packets_received = 0
@client_last_data_sent = 0
@client_last_data_received = 0
end
end
end
end
def handle_client
if @active_client && @active_client.connected?
message = @active_client.gets
unless message.empty?
@packet_handler.handle(message)
end
if Gosu.milliseconds > @last_heartbeat_sent + @heartbeat_interval
@last_heartbeat_sent = Gosu.milliseconds
@active_client.puts(PacketHandler.packet_heartbeart)
end
end
end
private def update_stats
if @active_client
# NOTE: Sent and Received are reversed for Server stats
@packets_sent += @active_client.packets_received - @client_last_packets_received
@packets_received += @active_client.packets_sent - @client_last_packets_sent
@data_sent += @active_client.data_received - @client_last_data_received
@data_received += @active_client.data_sent - @client_last_data_sent
@client_last_packets_sent = @active_client.packets_sent
@client_last_packets_received = @active_client.packets_received
@client_last_data_sent = @active_client.data_sent
@client_last_data_received = @active_client.data_received
end
end
end
end
end

19
lib/theme.rb Normal file
View File

@@ -0,0 +1,19 @@
module TAC
THEME = {
Label: {
font: "#{TAC::ROOT_PATH}/media/DejaVuSansCondensed.ttf",
text_size: 28
},
Button: {
background: TAC::Palette::TIMECRAFTERS_PRIMARY,
border_thickness: 1,
border_color: Gosu::Color.new(0xff_111111),
hover: {
background: TAC::Palette::TIMECRAFTERS_SECONDARY,
},
active: {
background: TAC::Palette::TIMECRAFTERS_TERTIARY
}
}
}
end

View File

@@ -1,9 +1,11 @@
module TAC
class Window < CyberarmEngine::Window
attr_reader :backend
def initialize(**args)
super(**args)
self.caption = "#{TAC::NAME} v#{TAC::VERSION} (#{TAC::RELEASE_NAME})"
@backend = Backend.new
push_state(TAC::States::Editor)
end