59 Commits

Author SHA1 Message Date
9b505b8201 Bump version 2021-09-30 11:07:01 -05:00
c609734357 Fixed confirm and tacnet dialogs not using correct titlebar background and border colors 2021-09-30 11:06:28 -05:00
fd6eb64232 Fixed some issues related to threading in game clock by using a queue 2021-09-30 10:51:49 -05:00
7bfc404413 Fixed mouse cursor always visible, even on game clock where it is preferred to be invisible while idle 2021-09-30 08:46:44 -05:00
870a3e4e8b Bump version 2021-09-29 16:26:43 -05:00
aa6d53dd5e Added rubber ducky, updated game clock randomizer 2021-09-29 16:26:24 -05:00
0f0009bcf0 Fixed crash due to renaming variable 2021-09-29 15:04:10 -05:00
7b7efabaf6 Added icon for game clock, updated game clock background image, made game clock background image auto-scale to fit window 2021-09-29 14:21:51 -05:00
2948b02f12 Dual screen game clock now working 2021-09-29 13:41:44 -05:00
44523b0bf2 Imported FTC Clock 2021-09-29 12:41:56 -05:00
7d2d44c52f Stubbed game clock page 2021-09-29 09:08:15 -05:00
a34d4bbeb2 Bump version 2021-09-23 14:39:09 -05:00
e915cbb72f Fixed crash when changing type of variable, fixed integer/long error sticking around when it's no longer a valid error 2021-09-23 14:38:33 -05:00
34c607d1a1 Bump version 2021-09-23 08:33:14 -05:00
9d75d2c09b Don't load local cyberarm_engine of building release, fixed FieldPlanner and DriverTeamRotationGenerator not excluded from 'no configuration loaded' redirect 2021-09-23 08:30:46 -05:00
606032c81d Bump version 2021-09-22 08:55:53 -05:00
6185728ea7 Added drive team rotation generator 2021-09-22 08:53:45 -05:00
02b16bab33 Used a fixed time step for Simulator 2021-09-19 21:17:59 -05:00
0facd003f0 Working thick lines for FieldPlanner 2021-09-19 20:32:00 -05:00
93c6430028 changes 2021-09-19 19:51:16 -05:00
3ee8881d43 Added Freight Frenzy field 2021-09-19 08:27:58 -05:00
81a827f0ac Added unit setting buttons to FieldPlanner and moved node count and total distance labels to status bar 2021-09-19 07:21:41 -05:00
9b53bd8a70 Initial FieldPlanner page for planning robot route 2021-09-18 22:49:55 -05:00
2969e3df9e Fixed Variable dialog erroneously saying that a valid type was invalid, removed old states and renamed NewEditor to Editor, updated dialogs with 2 side-by-side buttons to use 0.5 instead of 0.475 dynamic width, removed manual newlines in dialogs since TextBlock now supports word wrap. 2021-06-18 03:35:57 +00:00
64d4f08410 Fixed crash on Boot state due to changes in CyberarmEngine, spiced up Boot state with a few animations 2021-05-28 01:03:27 +00:00
c24a9d9673 Scroll lists to top when switching between groups and actions 2021-02-14 09:11:03 -06:00
27bae38291 Bump version, possibly fix crash on search 2021-02-13 14:41:41 -06:00
5fd494bb47 Fixed confirm dialog not able to commit 2021-02-13 13:13:22 -06:00
75d6de0b00 Added autofocus to edit dialogs first editline, added tab support for selecting next focusable element in edit dialogs, made valid? method be called in edit dialogs when fields change, simulator robot can now strafe side to side 2021-02-11 09:28:04 -06:00
3cc4c204a7 Allow edited action to keep its name when editing it (fixes: #1), made dialogs respond to enter and escape, Confirm dialog now shows dark red color for dangerous operations and prevents enter for accepting it 2021-02-10 22:21:53 -06:00
9999026969 Added PickPresetDialog for importing from preset, enable importing presets for groups and actions 2021-02-10 20:26:48 -06:00
4b1fdd9baf Make default config string be empty instead of nil 2021-02-10 19:42:29 -06:00
b625f53b6e Implement auto sorting 2021-02-10 14:29:44 -06:00
8934462c46 Add support for select and delete config packet emission 2021-02-10 13:08:21 -06:00
e27f1d1dca Initial presets editing support 2021-02-09 23:38:01 -06:00
ab55e8db5b Presets are now actually created, added notifications when creating preset. 2021-02-09 23:08:49 -06:00
988ef506ce Fixed tacnet error message thrown when received config is not the active config and would fail with wrong spec version instead of doing nothing. 2021-02-09 20:44:02 -06:00
d340e94d46 Use scrolling in editor, fixed border in editor, started work on presets, jump to group/action/variable from search is mostly working (may add highlighted background and scroll to in future) 2021-01-31 13:32:02 -06:00
9f54d21932 Page#page now accepts an options hash, booleans are now supported, search result buttons now redirect to to Editor page 2021-01-30 21:54:00 -06:00
4e9d3c0759 Initial implementation of search, updated PacketHandler to behave more more like android app 2021-01-30 21:27:16 -06:00
8659bdfe4a Copy pasta old editor layout and code into new editor 'page', made tacnet navigation button change foreground color based on TACNET connection state, tweaked theme. 2021-01-16 19:53:07 -06:00
22ab122604 Added .rubocop.yml, sync. 2020-12-14 16:07:43 -06:00
a1aa9a3396 changes 2020-12-12 11:55:19 -06:00
8a61b6263b Borderless experimenting 2020-12-10 20:37:00 -06:00
4adcff577a Make error message text longer 2020-12-09 19:10:36 -06:00
d36211cb5e New tacnet interface functional 2020-12-09 09:18:52 -06:00
691cafb697 Redesigning TCT 2020-12-08 22:18:54 -06:00
fe480202ac Correct field phantom line to add another column 2020-10-24 20:52:26 -05:00
28c159cc10 Update README.md 2020-10-06 19:50:51 -05:00
af074ad57f Added Ultimate Goal field to Simulator 2020-09-17 20:40:23 -05:00
5d267746dc Send requested config in download_config packet handler 2020-09-12 10:47:42 -05:00
df0d0df223 Dialog's no longer need a fixed height, misc. dialog tweaks. 2020-09-09 09:52:44 -05:00
b811a83c60 Fixed issues with Action dialog 2020-09-08 20:11:58 -05:00
9dc3caca0f TACNET is now able to dynamically sync configs on initial connection, added error sound, made tacnet status dialog update stats, made simulator clock stop after all robots have run out of 'states' to run, changed some dialogs titlebar and borders to be different colors, misc. other changes. 2020-09-08 19:25:04 -05:00
08ada79e5b Updated VariableDialog to use list_box instead of several buttons, make Variable store real type instead of mashing everything into value 2020-08-22 16:59:58 -05:00
d3fdc2d7dd TACNET fixes for multiple configs 2020-06-28 09:28:07 -05:00
1f37be3604 Added Rakefile, added Windows packaging config 2020-06-28 07:59:46 -05:00
f85426780b Added window icon 2020-06-28 07:16:38 -05:00
5e996019a9 Enable action enabled toggle button, refactored how Variable value is stored 2020-06-28 07:16:13 -05:00
73 changed files with 11708 additions and 718 deletions

10
.gitignore vendored
View File

@@ -1,3 +1,11 @@
pkg/*
data/**/*.json
data/settings.json
data/simulator.rb
data/simulator.rb
data/*.csv
media/sounds/*
!media/sounds/.gitkeep
media/particles/*
!media/particles/.gitkeep
media/music/*
!media/music/.gitkeep

8
.rubocop.yml Normal file
View File

@@ -0,0 +1,8 @@
Style/StringLiterals:
EnforcedStyle: double_quotes
Metrics/MethodLength:
Max: 40
Style/EmptyMethod:
EnforcedStyle: expanded

10
Gemfile
View File

@@ -1,3 +1,11 @@
source "https://rubygems.org"
gem "cyberarm_engine"
gem "cyberarm_engine"
gem "gosu_notifications"
gem "ffi"
gem "clipboard"
group :packaging do
gem "ocra"
gem "releasy"
end

37
Gemfile.lock Normal file
View File

@@ -0,0 +1,37 @@
GEM
remote: https://rubygems.org/
specs:
clipboard (1.3.6)
cri (2.1.0)
cyberarm_engine (0.19.1)
clipboard (~> 1.3.5)
excon (~> 0.78.0)
gosu (~> 1.1)
gosu_more_drawables (~> 0.3)
excon (0.78.1)
ffi (1.15.4-x64-mingw32)
gosu (1.2.0)
gosu_more_drawables (0.3.1)
gosu_notifications (0.1.0)
ocra (1.3.11)
rake (13.0.6)
releasy (0.2.3)
bundler (>= 1.2.1)
cri (~> 2.1.0)
ocra (~> 1.3.0)
rake (>= 0.9.2.2)
PLATFORMS
x64-mingw32
x86_64-linux
DEPENDENCIES
clipboard
cyberarm_engine
ffi
gosu_notifications
ocra
releasy
BUNDLED WITH
2.2.28

View File

@@ -1 +1 @@
# TimeCrafters Action Configurator for Desktop
# TimeCrafters Configuration Tool for Desktop

29
Rakefile Executable file
View File

@@ -0,0 +1,29 @@
require "releasy"
require 'bundler/setup' # Releasy requires that your application uses bundler.
require_relative "lib/version"
Releasy::Project.new do
name TAC::NAME
version TAC::VERSION
executable "timecrafters_configuration_tool.rb"
files [
"lib/**/*.*",
"data/.gitkeep",
"data/configs/.gitkeep",
"media/*.*",
"media/icons/*.*",
"media/fonts/*.*",
"media/sounds/.gitkeep",
"media/music/.gitkeep",
"media/particles/.gitkeep"
]
exclude_encoding # Applications that don't use advanced encoding (e.g. Japanese characters) can save build size with this.
verbose
add_build :windows_folder do
icon "media/icon.ico"
executable_type :console # Assuming you don't want it to run with a console window.
add_package :exe # Windows self-extracting archive.
end
end

View File

@@ -14,6 +14,12 @@ module TAC
@config.configuration.updated_at = Time.now
@config.configuration.revision += 1
@config_changed = true
save_config
if @tacnet.connected?
upload_config(@config.name)
end
end
def config_changed?
@@ -26,24 +32,49 @@ module TAC
end
end
def save_config(name)
json = @config.to_json
def save_config(name = nil, json = nil)
name = @config.name unless name
json = @config.to_json unless name && json
File.open("#{TAC::CONFIGS_PATH}/#{name}.json", "w") { |f| f.write json }
@config_changed = false
end
def upload_config
if @config && @tacnet.connected?
json = @config.to_json
@tacnet.puts(TAC::TACNET::PacketHandler.packet_upload_config(json))
def move_config(old_name, new_name)
if not File.exists?("#{TAC::CONFIGS_PATH}/#{old_name}.json") or
File.directory?("#{TAC::CONFIGS_PATH}/#{old_name}.json")
# move_config: Can not move config file "#{old_name}" does not exist!
return false
end
if File.exists?("#{TAC::CONFIGS_PATH}/#{new_name}.json") &&
!File.directory?("#{TAC::CONFIGS_PATH}/#{old_name}.json")
# move_config: Config file "#{new_name}" already exist!
return false
end
return FileUtils.mv(
"#{TAC::CONFIGS_PATH}/#{old_name}.json",
"#{TAC::CONFIGS_PATH}/#{new_name}.json"
)
end
def delete_config(config_name)
FileUtils.rm("#{TAC::CONFIGS_PATH}/#{config_name}.json") if File.exists?("#{TAC::CONFIGS_PATH}/#{config_name}.json")
end
def upload_config(config_name)
if @tacnet.connected?
json = Config.new(config_name).to_json
@tacnet.puts( TAC::TACNET::PacketHandler.packet_upload_config(config_name, json) )
end
end
def download_config
if @config && @tacnet.connected?
@tacnet.puts(TAC::TACNET::PacketHandler.packet_download_config)
def download_config(config_name)
if @tacnet.connected?
@tacnet.puts( TAC::TACNET::PacketHandler.packet_download_config(config_name) )
end
end
@@ -106,7 +137,7 @@ module TAC
data: {
hostname: TACNET::DEFAULT_HOSTNAME,
port: TACNET::DEFAULT_PORT,
config: nil,
config: "",
}
}
)

View File

@@ -1,7 +1,8 @@
module TAC
class Config
attr_reader :configuration, :groups, :presets
attr_reader :name, :configuration, :groups, :presets
def initialize(name)
@name = name
@configuration = nil
@groups = nil
@presets = nil
@@ -109,23 +110,24 @@ module TAC
end
class Action
attr_accessor :name, :enabled
attr_accessor :name, :comment, :enabled
attr_reader :variables
def initialize(name:, enabled:, variables:)
@name, @enabled = name, enabled
def initialize(name:, comment:, enabled:, variables:)
@name, @comment, @enabled = name, comment, enabled
@variables = variables
end
def to_json(*args)
{
name: @name,
comment: @comment,
enabled: @enabled,
variables: @variables
}.to_json(*args)
end
def self.from_json(hash)
Action.new(name: hash[:name], enabled: hash[:enabled], variables: hash[:variables].map { |h| Variable.from_json(h) })
Action.new(name: hash[:name], comment: hash[:comment], enabled: hash[:enabled], variables: hash[:variables].map { |h| Variable.from_json(h) })
end
end
@@ -138,13 +140,48 @@ module TAC
def to_json(*args)
{
name: @name,
type: @type,
value: @value
value: "#{Variable.encode_type(@type)}x#{@value}"
}.to_json(*args)
end
def self.from_json(hash)
Variable.new(name: hash[:name], type: hash[:type].to_sym, value: hash[:value])
type, value = hash[:value].split("x", 2)
type = Variable.decode_type(type)
Variable.new(name: hash[:name], type: type, value: value)
end
def self.encode_type(symbol)
symbol.to_s.chars.first.upcase
end
def self.decode_type(character)
case character.upcase
when "I"
:integer
when "F"
:float
when "D"
:double
when "L"
:long
when "S"
:string
when "B"
:boolean
end
end
def self.decode_value(type, string)
case type
when "I", "L", :integer, :long
Integer(string)
when "F", "D", :float, :double
Float(string)
when "S", :string
string
when "B", :boolean
string.downcase == "true"
end
end
end
end

View File

@@ -2,36 +2,43 @@ module TAC
class Dialog < CyberarmEngine::GuiState
def setup
theme(THEME)
background Gosu::Color.new(0x88_000000)
background Gosu::Color.new(0xaa_000000)
@title = @options[:title] ? @options[:title] : "#{self.class}"
@window_width, @window_height = window.width, window.height
@previous_state = window.previous_state
@dialog_root = stack width: 250, height: 400, border_thickness: 2, border_color: [TAC::Palette::TIMECRAFTERS_PRIMARY, TAC::Palette::TIMECRAFTERS_SECONDARY] do
@dialog_root = stack width: 0.25, border_thickness: 2, border_color: [TAC::Palette::TIMECRAFTERS_PRIMARY, TAC::Palette::TIMECRAFTERS_SECONDARY] do
# Title bar
flow width: 1.0, height: 0.1 do
@titlebar = flow width: 1.0 do
background [TAC::Palette::TIMECRAFTERS_PRIMARY, TAC::Palette::TIMECRAFTERS_SECONDARY]
# title
flow width: 0.855 do
label @title, text_size: THEME_SUBHEADING_TEXT_SIZE
flow width: 0.9 do
label @title, text_size: THEME_SUBHEADING_TEXT_SIZE, width: 1.0, text_align: :center, text_border: true, text_border_color: 0xff_222222, text_border_size: 1
end
# Buttons
flow width: 0.145 do
button get_image("#{TAC::ROOT_PATH}/media/icons/cross.png"), image_width: 24, **THEME_DANGER_BUTTON do
flow width: 0.0999 do
button get_image("#{TAC::ROOT_PATH}/media/icons/cross.png"), image_width: 1.0, **THEME_DANGER_BUTTON do
close
end
end
end
# Dialog body
stack width: 1.0, height: 0.9 do
build
@dialog_content = stack width: 1.0 do
end
end
@dialog_content.clear do
build
end
@root_container.recalculate
@root_container.recalculate
@root_container.recalculate
center_dialog
end
@@ -39,8 +46,47 @@ module TAC
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
@dialog_root.style.x = window.width / 2 - @dialog_root.width / 2
@dialog_root.style.y = window.height / 2 - @dialog_root.height / 2
end
def name_filter(text)
text.match(/[A-Za-z0-9._\- ]/) ? text : ""
end
def try_commit
end
def focus_next_element
elements = []
_deep_dive_interactive_elements(@dialog_content, elements)
element_index = elements.find_index(self.focused)
if element_index && elements.size.positive?
element = elements[element_index + 1]
element ||= elements.first
if element
request_focus(element)
end
end
end
def _deep_dive_interactive_elements(element, list)
element.children.each do |child|
if child.visible? && child.is_a?(CyberarmEngine::Element::EditLine) ||
child.is_a?(CyberarmEngine::Element::EditBox) ||
child.is_a?(CyberarmEngine::Element::CheckBox) ||
child.is_a?(CyberarmEngine::Element::ToggleButton) ||
child.is_a?(CyberarmEngine::Element::ListBox)
list << child
elsif child.visible? && child.is_a?(CyberarmEngine::Element::Container)
_deep_dive_interactive_elements(child, list)
end
end
end
def draw
@@ -54,10 +100,25 @@ module TAC
super
if window.width != @window_width or window.height != @window_height
center_dialog
request_recalculate
@window_width, @window_height = window.width, window.height
end
center_dialog
end
def button_down(id)
super
case id
when Gosu::KB_ENTER, Gosu::KB_RETURN
try_commit
when Gosu::KB_ESCAPE
close
when Gosu::KB_TAB
focus_next_element
end
end
def close

View File

@@ -0,0 +1,69 @@
module TAC
class Dialog
class ActionDialog < Dialog
def build
background Gosu::Color::GRAY
label "Name", width: 1.0, text_align: :center
@name_error = label "Error", color: TAC::Palette::TACNET_CONNECTION_ERROR
@name_error.hide
@name = edit_line @options[:action] ? @options[:action].name : "", filter: method(:name_filter), width: 1.0, autofocus: true
@name.subscribe(:changed) do |sender, value|
valid?
end
label "Comment", width: 1.0, text_align: :center
@comment = edit_line @options[:action] ? @options[:action].comment : "", width: 1.0
flow width: 1.0, margin_top: THEME_DIALOG_BUTTON_PADDING do
button "Cancel", width: 0.5 do
close
end
button @options[:action] ? @options[:accept_label] ? @options[:accept_label] : "Update" : "Add", width: 0.5 do |b|
try_commit
end
end
end
def try_commit
if valid?
if @options[:action]
@options[:callback_method].call(@options[:action], @name.value.strip, @comment.value.strip)
else
@options[:callback_method].call(@name.value.strip, @comment.value.strip)
end
close
end
end
def valid?
valid = true
name = @name.value.strip
if name.empty?
@name_error.value = "Error: Name cannot be blank or only whitespace."
@name_error.show
valid = false
### TODO: Handle case when renaming a cloned Action
elsif !@options[:cloning] && @options[:action] && @options[:action].name == name
@name_error.value = ""
@name_error.hide
elsif @options[:list].find { |action| action.name == name}
@name_error.value = "Error: Name is not unique!"
@name_error.show
valid = false
else
@name_error.value = ""
@name_error.hide
end
return valid
end
end
end
end

View File

@@ -5,10 +5,14 @@ module TAC
background Gosu::Color::GRAY
label @options[:message]
button "Close", width: 1.0 do
close
button "Close", width: 1.0, margin_top: THEME_DIALOG_BUTTON_PADDING do
try_commit
end
end
def try_commit
close
end
end
end
end

View File

@@ -2,14 +2,33 @@ module TAC
class Dialog
class ConfirmDialog < Dialog
def build
@dangerous = @options[:dangerous]
@dangerous ||= false
color = @dangerous ? Palette::DANGEROUS : Palette::ALERT
@dialog_root.style.default[:border_color] = [ color, darken(color, 50) ]
@titlebar.style.default[:background] = [ color, darken(color, 50) ]
background Gosu::Color::GRAY
label @options[:message]
flow width: 1.0 do
button "Cancel", width: 0.475 do
flow width: 1.0, margin_top: THEME_DIALOG_BUTTON_PADDING do
button "Cancel", width: 0.5 do
close
end
button "Okay", width: 0.475 do
button "Proceed", width: 0.5, **TAC::THEME_DANGER_BUTTON do
try_commit(true)
end
end
def try_commit(force = false)
if !@dangerous
close
@options[:callback_method].call
elsif @dangerous && force
close
@options[:callback_method].call

View File

@@ -5,47 +5,58 @@ module TAC
def build
background Gosu::Color::GRAY
flow width: 1.0 do
label "Name", width: 0.25
@name = edit_line @options[:renaming] ? @options[:renaming].name : "", filter: method(:filter), width: 0.70
end
label "Name", width: 1.0, text_align: :center
@name_error = label "", color: TAC::Palette::TACNET_CONNECTION_ERROR
@name_error.hide
@name = edit_line @options[:renaming] ? @options[:renaming].name : "", filter: method(:name_filter), width: 1.0, autofocus: true
@name.subscribe(:changed) do |sender, value|
valid?
end
flow width: 1.0 do
button "Cancel", width: 0.475 do
flow width: 1.0, margin_top: THEME_DIALOG_BUTTON_PADDING do
button "Cancel", width: 0.5 do
close
end
accept_label = @options[:renaming] ? "Update" : "Add"
accept_label = @options[:accept_label] if @options[:accept_label]
button accept_label, width: 0.475 do
unless valid?
else
if @options[:renaming]
@options[:callback_method].call(@options[:renaming], @name.value.strip)
else
@options[:callback_method].call(@name.value.strip)
end
close
end
button accept_label, width: 0.5 do
try_commit
end
end
end
def try_commit
if valid?
if @options[:renaming]
@options[:callback_method].call(@options[:renaming], @name.value.strip)
else
@options[:callback_method].call(@name.value.strip)
end
close
end
end
def valid?
name = @name.value.strip
if @name.value.strip.empty?
@name_error.value = "Name cannot be blank.\nName cannot only be whitespace."
@name_error.value = "Name cannot be blank. Name cannot only be whitespace."
@name_error.show
return false
elsif @options[:list] && @options[:list].find { |i| i.name == @name.value.strip }
### TODO: Handle case when renaming a cloned Group
# elsif @options[:renaming] && @options[:renaming].name == name
# @name_error.value = ""
# @name_error.hide
# return true
elsif @options[:list] && @options[:list].find { |i| i.name == name }
@name_error.value = "Name is not unique!"
@name_error.show
@@ -57,10 +68,6 @@ module TAC
return true
end
end
def filter(text)
text.match(/[A-Za-z0-9._\- ]/) ? text : ""
end
end
end
end

View File

@@ -0,0 +1,26 @@
module TAC
class Dialog
class PickPresetDialog < Dialog
def build
@limit = @options[:limit]
list = window.backend.config.presets.groups if @limit == :groups
list = window.backend.config.presets.actions if @limit == :actions
background Gosu::Color::GRAY
stack(width: 1.0, height: 512, scroll: true) do
list.each do |item|
button item.name, width: 1.0 do
close
@options[:callback_method].call(item)
end
end
end
end
def try_commit
end
end
end
end

View File

@@ -0,0 +1,29 @@
module TAC
class Dialog
class TACNETDialog < Dialog
def build
@dialog_root.style.default[:border_color] = [ Palette::TACNET_PRIMARY, Palette::TACNET_SECONDARY ]
@titlebar.style.default[:background] = [ Palette::TACNET_PRIMARY, Palette::TACNET_SECONDARY ]
background Gosu::Color::GRAY
label @options[:message], width: 1.0
@sound = Gosu::Sample.new("#{TAC::ROOT_PATH}/media/error_alarm.ogg").play(1, 1, true)
button "Close", width: 1.0, margin_top: THEME_DIALOG_BUTTON_PADDING do
try_commit
end
end
def try_commit
close
end
def close
super
@sound.stop
end
end
end
end

View File

@@ -0,0 +1,28 @@
module TAC
class Dialog
class TACNETStatusDialog < Dialog
def build
background Gosu::Color::GRAY
@message_label = label $window.backend.tacnet.full_status
button "Close", width: 1.0, margin_top: THEME_DIALOG_BUTTON_PADDING do
try_commit
end
@timer = CyberarmEngine::Timer.new(1000.0) do
@message_label.value = $window.backend.tacnet.full_status
end
end
def try_commit
close
end
def update
super
@timer.update
end
end
end
end

View File

@@ -6,114 +6,141 @@ module TAC
@type = @options[:variable].type if @options[:variable]
label "Name"
label "Name", width: 1.0, text_align: :center
@name_error = label "Error", color: TAC::Palette::TACNET_CONNECTION_ERROR
@name_error.hide
@name = edit_line @options[:variable] ? @options[:variable].name : ""
@name = edit_line @options[:variable] ? @options[:variable].name : "", filter: method(:name_filter), width: 1.0, autofocus: true
@name.subscribe(:changed) do |sender, value|
valid?
end
label "Type"
label "Type", width: 1.0, text_align: :center
@type_error = label "Error", color: TAC::Palette::TACNET_CONNECTION_ERROR
@type_error.hide
# TODO: Add dropdown menus to CyberarmEngine
flow width: 1.0 do
[:float, :double, :integer, :long, :string, :boolean].each do |btn|
button btn do
@type = btn
@value_container.show
end
@var_type = list_box items: [:float, :double, :integer, :long, :string, :boolean], choose: @type ? @type : :double, width: 1.0 do |item|
@type = item.to_sym
if @type == :boolean
@value.hide
@value_boolean.show
else
@value.show
@value_boolean.hide
end
valid?
end
@type ||= @var_type.value.to_sym
@value_container = stack width: 1.0 do
label "Value", width: 1.0, text_align: :center
@value_error = label "Error", color: TAC::Palette::TACNET_CONNECTION_ERROR
@value_error.hide
@value = edit_line @options[:variable] ? @options[:variable].value : "", width: 1.0
@value_boolean = check_box "Boolean", checked: @options[:variable] ? @options[:variable].value == "true" : false
@value.subscribe(:changed) do |sender, value|
valid?
end
unless @options[:variable] && @options[:variable].type == :boolean
@value_boolean.hide
else
@value.hide
end
end
@value_container = stack width: 1.0 do
label "Value"
@value_error = label "Error", color: TAC::Palette::TACNET_CONNECTION_ERROR
@value_error.hide
@value = edit_line @options[:variable] ? @options[:variable].value : ""
end
flow width: 1.0 do
button "Cancel", width: 0.475 do
flow width: 1.0, margin_top: THEME_DIALOG_BUTTON_PADDING do
button "Cancel", width: 0.5 do
close
end
button @options[:variable] ? "Update" : "Add", width: 0.475 do |b|
if valid?
if @options[:variable]
@options[:callback_method].call(@options[:variable], @name.value.strip, @type, @value.value.strip)
else
@options[:callback_method].call(@name.value.strip, @type, @value.value.strip)
end
close
end
button @options[:variable] ? "Update" : "Add", width: 0.5 do |b|
try_commit
end
end
end
def try_commit
if valid?
value = @type == :boolean ? @value_boolean.value.to_s : @value.value.strip
if @options[:variable]
@options[:callback_method].call(@options[:variable], @name.value.strip, @type, value)
else
@options[:callback_method].call(@name.value.strip, @type, value)
end
close
end
end
def valid?
valid = true
if @name.value.strip.empty?
@name_error.value = "Error: Name cannot be blank\n or only whitespace."
@name_error.value = "Error: Name cannot be blank or only whitespace."
@name_error.show
valid = false
else
@name_error.value = ""
@name_error.hide
end
if not @type
@type_error.value = "Error: Type not set."
@type_error.show
valid = false
else
@type_error.value = ""
@type_error.hide
end
if [:integer, :float, :double, :long].include?(@type)
if @value.value.strip.empty?
@value_error.value = "Error: Value cannot be blank\n or only whitespace."
@value_error.value = "Error: Numeric value cannot be blank or only whitespace."
@value_error.show
valid = false
elsif [:integer, :long].include?(@type)
begin
Integer(@value.value.strip)
rescue
@value_error.value = "Error: Invalid value,\nexpected whole number."
@value_error.value = "Error: Invalid value, expected whole number."
@value_error.show
valid = false
end
elsif [:float, :double].include?(@type)
begin
Float(@value.value.strip)
rescue
@value_error.value = "Error: Invalid value,\nexpected decimal number."
@value_error.value = "Error: Invalid value, expected decimal number."
@value_error.show
valid = false
end
else
@value_error.value = ""
@value_error.hide
end
elsif @type == :string
if @value.value.strip.empty?
@value_error.value = "Error: Value cannot be blank\n or only whitespace."
@value_error.value = "Error: Value cannot be blank or only whitespace."
@value_error.show
valid = false
end
elsif @type == :boolean
@value_error.value = "Error: Boolean not yet supported."
@value_error.show
valid = false
else
@value_error.value = "Error: Type not set."
@value_error.value = "Error: Type not set or type #{@type.inspect} is not valid."
@value_error.show
valid = false
end
if valid
@value_error.value = ""
@value_error.hide
@type_error.value = ""
@type_error.hide
end
return valid
end
end
end
end
end

70
lib/game_clock/clock.rb Normal file
View File

@@ -0,0 +1,70 @@
module TAC
class PracticeGameClock
class Clock
CLOCK_SIZE = Gosu.screen_height
TITLE_SIZE = 128
attr_reader :title, :controller
def initialize
@title = CyberarmEngine::Text.new("FIRST TECH CHALLENGE", size: TITLE_SIZE, text_shadow: true, y: 10, color: Gosu::Color::GRAY)
@title.x = $window.width / 2 - @title.width / 2
@text = CyberarmEngine::Text.new(":1234567890", size: CLOCK_SIZE, text_border: true, border_size: 2, border_color: Gosu::Color::GRAY)
@text.width # trigger font-eager loading
@title.z, @text.z = -1, -1
@controller = nil
end
def controller=(controller)
@controller = controller
end
def draw
@title.draw
@text.draw
end
def update
@title.x = $window.width / 2 - @title.width / 2
if @controller
@text.color = @controller.display_color
@text.text = clock_time(@controller.time_left)
else
@text.color = Gosu::Color::WHITE
@text.text = "0:00"
end
@text.x = $window.width / 2 - @text.textobject.text_width("0:00") / 2
@text.y = $window.height / 2 - @text.height / 2
@controller&.update
end
def active?
if @controller
@controller.clock? || @controller.countdown?
else
false
end
end
def value
@text.text
end
def clock_time(time_left)
minutes = ((time_left + 0.5) / 60.0).floor
seconds = time_left.round % 60
seconds = "0#{seconds}" if seconds < 10
return "#{minutes}:#{seconds}" if time_left.round.even?
return "#{minutes}<c=999999>:</c>#{seconds}" if time_left.round.odd?
end
end
end
end

View File

@@ -0,0 +1,136 @@
module TAC
class PracticeGameClock
class ClockController
Event = Struct.new(:event, :trigger_after, :arguments)
include EventHandlers
def self.create_event(event, trigger_after, arguments = nil)
arguments = [arguments] unless arguments.is_a?(Array) || arguments == nil
Event.new(event, trigger_after, arguments)
end
AUTONOMOUS = [
create_event(:change_clock, 0.0, "2:30"),
create_event(:change_countdown, 0.0, "0:03"),
create_event(:change_color, 0.0, :red),
create_event(:change_display, 0.0, :countdown),
create_event(:start_countdown, 0.0),
create_event(:play_sound, 0.0, :autonomous_countdown),
create_event(:change_color, 3.0, :white),
create_event(:change_display, 3.0, :clock),
create_event(:play_sound, 3.0, :autonomous_start),
create_event(:change_display, 3.0, :clock),
create_event(:stop_countdown, 3.0),
create_event(:start_clock, 3.0),
create_event(:play_sound, 33.0, :autonomous_ended),
create_event(:stop_clock, 33.0),
].freeze
PRE_TELEOP = [
create_event(:change_color, 33.0, :orange),
create_event(:change_countdown, 33.0, "0:08"),
create_event(:change_display, 33.0, :countdown),
create_event(:start_countdown, 33.0),
create_event(:play_sound, 34.5, :teleop_pickup_controllers),
create_event(:change_color, 37.0, :red),
create_event(:play_sound, 38.0, :teleop_countdown),
create_event(:stop_countdown, 41.0),
].freeze
TELEOP_ENDGAME = [
create_event(:change_color, 131.0, :white),
create_event(:change_clock, 131.0, "0:30"),
create_event(:start_clock, 131.0),
create_event(:play_sound, 131.0, :end_game),
create_event(:play_sound, 158.0, :autonomous_countdown),
create_event(:play_sound, 161.0, :end_match),
create_event(:stop_clock, 161.0),
].freeze
TELEOP = [
create_event(:change_color, 41.0, :white),
create_event(:change_clock, 41.0, "2:00"),
create_event(:play_sound, 41.0, :teleop_started),
create_event(:change_display, 41.0, :clock),
create_event(:start_clock, 41.0),
TELEOP_ENDGAME
].flatten.freeze
FULL_TELEOP = [
PRE_TELEOP,
TELEOP,
TELEOP_ENDGAME,
].flatten.freeze
FULL_MATCH = [
# Autonomous
AUTONOMOUS,
FULL_TELEOP
].flatten.freeze
attr_reader :display_color
def initialize(elapsed_time = 0, events = [])
@events = events.dup
@last_update = Gosu.milliseconds
@elapsed_time = elapsed_time
@display = :clock
@clock_time = 0.0
@countdown_time = 0.0
@clock_running = false
@countdown_running = false
@display_color = Gosu::Color::WHITE
end
def update
dt = (Gosu.milliseconds - @last_update) / 1000.0
update_active_timer(dt)
@events.select { |event| event.trigger_after <= @elapsed_time }.each do |event|
@events.delete(event)
if event.arguments
self.send(event.event, *event.arguments)
else
self.send(event.event)
end
end
@last_update = Gosu.milliseconds
end
def update_active_timer(dt)
if @clock_running
@clock_time -= dt
elsif @countdown_running
@countdown_time -= dt
end
@elapsed_time += dt
end
def clock?
@clock_running
end
def countdown?
@countdown_running
end
def time_left
if @clock_running
return @clock_time
elsif @countdown_running
return @countdown_time
else
return 60 * 2 + 30
end
end
end
end
end

View File

@@ -0,0 +1,107 @@
module TAC
class PracticeGameClock
class ClockProxy
attr_reader :queue, :clock
def initialize(clock, jukebox)
@clock = clock
@jukebox = jukebox
@queue = []
@callbacks = {}
end
def enqueue(&block)
@queue << block
end
def register(callback, method)
@callbacks[callback] = method
end
def start_clock(mode)
return if @clock.active? || $window.current_state.is_a?(Randomizer)
@clock.controller = case mode
when :full_match
ClockController.new(0, ClockController::FULL_MATCH)
when :autonomous
ClockController.new(0, ClockController::AUTONOMOUS)
when :full_teleop
ClockController.new(33.0, ClockController::FULL_TELEOP)
when :teleop_only
ClockController.new(41.0, ClockController::TELEOP)
when :endgame_only
ClockController.new(131.0, ClockController::TELEOP_ENDGAME)
else
nil
end
end
def abort_clock
@clock.controller&.play_sound(:abort_match) if @clock.active?
@clock.controller = nil
end
def set_clock_title(string)
@clock.title.text = string.to_s
@clock.title.x = $window.width / 2 - @clock.title.width / 2
end
def get_clock_title(string)
@clock.title
end
def jukebox_previous_track
@jukebox.previous_track
end
def jukebox_next_track
@jukebox.next_track
end
def jukebox_stop
@jukebox.stop
end
def jukebox_play
@jukebox.play
end
def jukebox_pause
@jukebox.pause
end
def jukebox_set_volume(float)
@jukebox.set_volume(float)
end
def jukebox_volume
@jukebox.volume
end
def jukebox_current_track
@jukebox.now_playing
end
def jukebox_set_sound_effects(boolean)
@jukebox.set_sfx(boolean)
end
def jukebox_volume_changed(float)
@callbacks[:volume_changed]&.call(float)
end
def jukebox_track_changed(name)
@callbacks[:track_changed]&.call(name)
end
def randomizer_changed(boolean)
@callbacks[:randomizer_changed]&.call(boolean)
end
def shutdown!
end
end
end
end

View File

@@ -0,0 +1,89 @@
module TAC
class PracticeGameClock
module EventHandlers
### Clock ###
def start_clock
@clock_running = true
end
def stop_clock
@clock_running = false
end
def change_clock(value)
@clock_time = time_from_string(value)
end
### Countdown ###
def start_countdown
@countdown_running = true
end
def stop_countdown
@countdown_running = false
end
def change_countdown(value)
@countdown_time = time_from_string(value)
end
def change_display(display)
end
def change_color(color)
out = case color
when :white
Gosu::Color::WHITE
when :orange
Gosu::Color.rgb(150, 75, 0)
when :red
Gosu::Color.rgb(150, 0, 0)
end
@display_color = out
end
private def time_from_string(string)
split = string.split(":")
minutes = (split.first.to_i) * 60
seconds = (split.last.to_i)
return minutes + seconds
end
def play_sound(sound)
path = nil
case sound
when :autonomous_countdown
path = "media/sounds/3-2-1.wav"
when :autonomous_start
path = "media/sounds/charge.wav"
when :autonomous_ended
path = "media/sounds/endauto.wav"
when :teleop_pickup_controllers
path = "media/sounds/Pick_Up_Controllers.wav"
when :abort_match
path = "media/sounds/fogblast.wav"
when :teleop_countdown
path = "media/sounds/3-2-1.wav"
when :teleop_started
path = "media/sounds/firebell.wav"
when :end_game
path = "media/sounds/factwhistle.wav"
when :end_match
path = "media/sounds/endmatch.wav"
end
path = "#{ROOT_PATH}/#{path}"
if path && File.exist?(path) && !File.directory?(path)
Jukebox::SAMPLES[path] = Gosu::Sample.new(path) unless Jukebox::SAMPLES[path].is_a?(Gosu::Sample)
Jukebox::SAMPLES[path].play
else
warn "WARNING: Sample for #{sound.inspect} could not be found at '#{path}'"
end
end
end
end
end

144
lib/game_clock/jukebox.rb Normal file
View File

@@ -0,0 +1,144 @@
module TAC
class PracticeGameClock
class Jukebox
MUSIC = Dir.glob(ROOT_PATH + "/media/music/*.*").freeze
SAMPLES = {}
if File.exist?(ROOT_PATH + "/media/sounds/skystone")
BEEPS_AND_BOOPS = Dir.glob(ROOT_PATH + "/media/sounds/skystone/*.*").freeze
end
attr_reader :volume, :now_playing
def initialize(clock)
@clock = clock
@order = MUSIC.shuffle
@now_playing = ""
@playing = false
@song = nil
@volume = 1.0
@last_check_time = Gosu.milliseconds
@last_sfx_time = Gosu.milliseconds
@sfx_random_interval = generate_sfx_period
@play_sfx = true
if defined?(BEEPS_AND_BOOPS)
BEEPS_AND_BOOPS.each do |beep|
SAMPLES[beep] = Gosu::Sample.new(beep)
end
end
end
def update
return unless Gosu.milliseconds - @last_check_time >= 2000.0
@last_check_time = Gosu.milliseconds
if @song && !@song.playing? && !@song.paused?
next_track if @playing
end
if @play_sfx && defined?(BEEPS_AND_BOOPS)
play_sfx
end
end
def play_sfx
if Gosu.milliseconds - @last_sfx_time >= @sfx_random_interval
@last_sfx_time = Gosu.milliseconds
@sfx_random_interval = generate_sfx_period
pan = rand(0.49999..0.50001)
volume = rand(0.75..1.0)
speed = rand(0.5..1.25)
SAMPLES[BEEPS_AND_BOOPS.sample].play_pan(pan, volume, speed) unless @clock.active?
end
end
def generate_sfx_period
# rand(15..120) * 1000.0
rand(5..10) * 1000.0
end
def set_sfx(boolean)
@play_sfx = boolean
end
def play_sfx?
@play_sfx
end
def play
if @song && @song.paused?
@song.play
else
return false unless @order.size > 0
@current_song = @order.first
@song = Gosu::Song.new(@current_song)
@song.volume = @volume
@song.play
@now_playing = File.basename(@current_song)
@order.rotate!(1)
end
@playing = true
end
def pause
@playing = false
@song.pause if @song
end
def song
@song
end
def stop
@song.stop if @song
@playing = false
@now_playing = ""
end
def previous_track
return false unless @order.size > 0
@song.stop if @song
@order.rotate!(-1)
@current_song = @order.first
@song = Gosu::Song.new(@current_song)
@song.volume = @volume
@song.play
@playing = true
@now_playing = File.basename(@current_song)
end
def next_track
return false unless @order.size > 0
@song.stop if @song
@current_song = @order.first
@song = Gosu::Song.new(@current_song)
@song.volume = @volume
@song.play
@order.rotate!(1)
@playing = true
@now_playing = File.basename(@current_song)
end
def current_track
@current_song
end
def set_volume(float)
@volume = float
@volume = @volume.clamp(0.1, 1.0)
@song.volume = @volume if @song
end
end
end
end

28
lib/game_clock/logger.rb Normal file
View File

@@ -0,0 +1,28 @@
module TAC
class PracticeGameClock
class ClockNet
class Logger
def printer(message)
# return
puts "#{Time.now.strftime("%Y-%m-%d %H:%M:%S %Z")} #{message}"
end
def i(tag, message)
printer("INFO #{tag}: #{message}")
end
def d(tag, message)
printer("DEBUG #{tag}: #{message}")
end
def e(tag, message)
printer("ERROR #{tag}: #{message}")
end
end
end
def log
@logger ||= ClockNet::Logger.new
end
end
end

View File

@@ -0,0 +1,162 @@
require "securerandom"
module TAC
class PracticeGameClock
class ClockNet
class Client
TAG = "ClockNet|Client"
CHUNK_SIZE = 4096
PACKET_TAIL = "\r\n\n"
attr_reader :uuid, :read_queue, :write_queue, :socket,
:packets_sent, :packets_received,
:data_sent, :data_received
attr_accessor :sync_interval, :last_socket_error, :socket_error
def initialize
@uuid = SecureRandom.uuid
@read_queue = []
@write_queue = []
@sync_interval = 100
@last_socket_error = nil
@socket_error = false
@bound = false
@packets_sent, @packets_received = 0, 0
@data_sent, @data_received = 0, 0
end
def uuid=(id)
@uuid = id
end
def socket=(socket)
@socket = socket
@bound = true
listen
end
def listen
Thread.new do
while connected?
# Read from socket
while message_in = read
if message_in.empty?
break
else
log.i(TAG, "Read: " + message_in)
@read_queue << message_in
@packets_received += 1
@data_received += message_in.length
end
end
sleep @sync_interval / 1000.0
end
end
Thread.new do
while connected?
# Write to socket
while message_out = @write_queue.shift
write(message_out)
@packets_sent += 1
@data_sent += message_out.to_s.length
log.i(TAG, "Write: " + message_out.to_s)
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)
log.i(TAG, "Writing to Queue: " + message)
message = gets
end
end
def socket_error?
@socket_error
end
def connected?
if closed? == true || closed? == nil
false
else
true
end
end
def closed?
@socket.closed? if @socket
end
def write(message)
begin
@socket.puts("#{message}#{PACKET_TAIL}")
rescue => error
@last_socket_error = error
@socket_error = true
log.e(TAG, error.message)
close
end
end
def read
begin
message = @socket.gets
rescue => error
@last_socket_error = error
@socket_error = true
message = ""
end
return message.strip
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
end

View File

@@ -0,0 +1,97 @@
module TAC
class PracticeGameClock
class ClockNet
class Connection
TAG = "ClockNet|Connection"
attr_reader :hostname, :port, :client, :proxy_object
def initialize(hostname: "localhost", port: 4567, proxy_object:)
@hostname = hostname
@port = port
@proxy_object = proxy_object
@client = nil
@last_sync_time = Gosu.milliseconds
@sync_interval = SYNC_INTERVAL
@last_heartbeat_sent = Gosu.milliseconds
@heartbeat_interval = HEARTBEAT_INTERVAL
@connection_handler = proc do
handle_connection
end
@packet_handler = PacketHandler.new(host_is_a_connection: true, proxy_object: @proxy_object)
end
def connect
return if @client
@client = Client.new
Thread.new do
begin
@client.socket = Socket.tcp(@hostname, @port, connect_timeout: 5)
@client.socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1)
log.i(TAG, "Connected to: #{@hostname}:#{@port}")
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
log.e(TAG, error)
if @client
@client.close
@client.last_socket_error = error
@client.socket_error = true
end
end
end
end
def handle_connection
if @client && @client.connected?
message = @client.gets
@packet_handler.handle(message) if message
if Gosu.milliseconds > @last_heartbeat_sent + @heartbeat_interval
@last_heartbeat_sent = Gosu.milliseconds
@client.puts(PacketHandler.packet_heartbeat)
end
sleep @sync_interval / 1000.0
end
end
def puts(packet)
@client.puts(packet)
end
def gets
@client.gets
end
def connected?
@client.connected? if @client
end
def closed?
@client.closed? if @client
end
def close
@client.close if @client
end
end
end
end
end

View File

@@ -0,0 +1,109 @@
module TAC
class PracticeGameClock
class ClockNet
SYNC_INTERVAL = 250
HEARTBEAT_INTERVAL = 1_500
class Packet
PROTOCOL_VERSION = 1
PROTOCOL_SEPERATOR = "|"
PROTOCOL_HEARTBEAT = "heartbeat"
PACKET_TYPES = {
handshake: 0,
heartbeat: 1,
error: 2,
shutdown: 3,
start_clock: 10,
abort_clock: 11,
set_clock_title: 20,
get_clock_title: 21,
clock_title: 22,
clock_time: 23,
randomizer_visible: 27,
jukebox_previous_track: 30,
jukebox_next_track: 31,
jukebox_stop: 32,
jukebox_play: 33,
jukebox_pause: 34,
jukebox_set_volume: 35,
jukebox_get_volume: 36,
jukebox_volume: 37,
jukebox_get_current_track: 38,
jukebox_current_track: 39,
jukebox_get_sound_effects: 40,
jukebox_set_sound_effects: 41,
jukebox_sound_effects: 42,
}
def self.from_stream(message)
slice = message.split("|", 4)
if slice.size < 4
warn "Failed to split packet along first 4 " + PROTOCOL_SEPERATOR + ". Raw return: " + slice.to_s
return nil
end
if slice.first != PROTOCOL_VERSION.to_s
warn "Incompatible protocol version received, expected: " + PROTOCOL_VERSION.to_s + " got: " + slice.first
return nil
end
unless valid_packet_type?(Integer(slice[1]))
warn "Unknown packet type detected: #{slice[1]}"
return nil
end
protocol_version = Integer(slice[0])
type = PACKET_TYPES.key(Integer(slice[1]))
content_length = Integer(slice[2])
body = slice[3]
raise "Type is #{type.inspect} [#{type.class}]" unless type.is_a?(Symbol)
return Packet.new(protocol_version, type, content_length, body)
end
def self.create(packet_type, body)
Packet.new(PROTOCOL_VERSION, PACKET_TYPES.key(packet_type), body.length, body)
end
def self.valid_packet_type?(packet_type)
PACKET_TYPES.values.find { |t| t == packet_type }
end
attr_reader :protocol_version, :type, :content_length, :body
def initialize(protocol_version, type, content_length, body)
@protocol_version = protocol_version
@type = type
@content_length = content_length
@body = body
end
def encode_header
string = ""
string += protocol_version.to_s
string += PROTOCOL_SEPERATOR
string += PACKET_TYPES[type].to_s
string += PROTOCOL_SEPERATOR
string += content_length.to_s
string += PROTOCOL_SEPERATOR
return string
end
def valid?
true
end
def to_s
"#{encode_header}#{body}"
end
end
end
end
end

View File

@@ -0,0 +1,320 @@
module TAC
class PracticeGameClock
class ClockNet
class PacketHandler
TAG = "ClockNet|PacketHandler"
def initialize(host_is_a_connection: false, proxy_object:)
@host_is_a_connection = host_is_a_connection
@proxy_object = proxy_object
end
def handle(message)
packet = Packet.from_stream(message)
if packet
log.i(TAG, "Received packet of type: #{packet.type}")
hand_off(packet)
else
log.d(TAG, "Rejected raw packet: #{message}")
end
end
def hand_off(packet)
case packet.type
when :handshake
handle_handshake(packet)
when :heartbeat
handle_heartbeat(packet)
when :error
handle_error(packet)
when :start_clock
handle_start_clock(packet)
when :abort_clock
handle_abort_clock(packet)
when :get_clock_title
handle_get_clock_title(packet)
when :set_clock_title
handle_set_clock_title(packet)
when :clock_title
handle_clock_title(packet)
when :jukebox_previous_track
handle_jukebox_previous_track(packet)
when :jukebox_next_track
handle_jukebox_next_track(packet)
when :jukebox_play
handle_jukebox_play(packet)
when :jukebox_pause
handle_jukebox_pause(packet)
when :jukebox_stop
handle_jukebox_stop(packet)
when :jukebox_set_volume
handle_jukebox_set_volume(packet)
when :jukebox_volume
handle_jukebox_volume(packet)
when :jukebox_set_sound_effects
handle_jukebox_set_sound_effects(packet)
when :jukebox_current_track
handle_jukebox_current_track(packet)
when :clock_time
handle_clock_time(packet)
when :randomizer_visible
handle_randomizer_visible(packet)
when :shutdown
handle_shutdown(packet)
else
log.d(TAG, "No hand off available for packet type: #{packet.type}")
end
end
def handle_handshake(packet)
if @host_is_a_connection
end
end
# TODO: Reset socket timeout
def handle_heartbeat(packet)
end
# TODO: Handle errors
def handle_error(packet)
title, message = packet.body.split(Packet::PROTOCOL_SEPERATOR, 2)
log.e(TAG, "Remote error: #{title}: #{message}")
end
def handle_start_clock(packet)
return if @host_is_a_connection
@proxy_object.enqueue do
@proxy_object.start_clock(packet.body.to_sym)
end
end
def handle_abort_clock(packet)
return if @host_is_a_connection
@proxy_object.abort_clock
end
def handle_set_clock_title(packet)
return if @host_is_a_connection
title = packet.body
@proxy_object.enqueue do
@proxy_object.set_clock_title(title)
end
end
def handle_get_clock_title(packet)
return if @host_is_a_connection
RemoteControl.server.active_client.puts(Packet.clock_title(@proxy_object.clock.title))
end
def handle_jukebox_previous_track(packet)
return if @host_is_a_connection
@proxy_object.jukebox_previous_track
RemoteControl.server.active_client.puts(PacketHandler.packet_jukebox_current_track(@proxy_object.jukebox_current_track))
end
def handle_jukebox_next_track(packet)
return if @host_is_a_connection
@proxy_object.jukebox_next_track
RemoteControl.server.active_client.puts(PacketHandler.packet_jukebox_current_track(@proxy_object.jukebox_current_track))
end
def handle_jukebox_play(packet)
return if @host_is_a_connection
@proxy_object.jukebox_play
RemoteControl.server.active_client.puts(PacketHandler.packet_jukebox_current_track(@proxy_object.jukebox_current_track))
end
def handle_jukebox_pause(packet)
return if @host_is_a_connection
@proxy_object.jukebox_pause
RemoteControl.server.active_client.puts(PacketHandler.packet_jukebox_current_track(@proxy_object.jukebox_current_track))
end
def handle_jukebox_stop(packet)
return if @host_is_a_connection
@proxy_object.jukebox_stop
RemoteControl.server.active_client.puts(PacketHandler.packet_jukebox_current_track(@proxy_object.jukebox_current_track))
end
def handle_jukebox_set_volume(packet)
return if @host_is_a_connection
float = packet.body.to_f
float = float.clamp(0.0, 1.0)
@proxy_object.jukebox_set_volume(float)
float = @proxy_object.jukebox_volume
RemoteControl.server.active_client.puts(PacketHandler.packet_jukebox_volume(float))
end
def handle_jukebox_get_volume(packet)
return if @host_is_a_connection
float = @proxy_object.jukebox_volume
RemoteControl.server.active_client.puts(PacketHandler.packet_jukebox_volume(float))
end
def handle_jukebox_volume(packet)
return unless @host_is_a_connection
float = packet.body.to_f
@proxy_object.enqueue do
@proxy_object.volume_changed(float)
end
end
def handle_jukebox_set_sound_effects(packet)
return if @host_is_a_connection
boolean = packet.body == "true"
@proxy_object.enqueue do
@proxy_object.jukebox_set_sound_effects(boolean)
end
end
def handle_jukebox_current_track(packet)
return unless @host_is_a_connection
@proxy_object.enqueue do
@proxy_object.track_changed(packet.body)
end
end
def handle_clock_time(packet)
return unless @host_is_a_connection
@proxy_object.enqueue do
@proxy_object.clock_changed(packet.body)
end
end
def handle_randomizer_visible(packet)
boolean = packet.body == "true"
boolean = false if @proxy_object.is_a?(ClockProxy) && @proxy_object.clock.active?
@proxy_object.enqueue do
@proxy_object.randomizer_changed(boolean)
end
return if @host_is_a_connection
# Send confirmation to client
RemoteControl.server.active_client.puts(PacketHandler.packet_randomizer_visible(boolean))
end
def handle_shutdown(packet)
unless @host_is_a_connection
# RemoteControl.server.close
# $window.close
Gosu::Song.current_song&.stop
exit
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_HEARTBEAT)
end
def self.packet_error(error_code, message)
Packet.create(Packet::PACKET_TYPES[:error], error_code.to_s, message.to_s)
end
def self.packet_start_clock(mode)
Packet.create(Packet::PACKET_TYPES[:start_clock], mode.to_s)
end
def self.packet_abort_clock
Packet.create(Packet::PACKET_TYPES[:abort_clock], "")
end
def self.packet_set_clock_title(string)
Packet.create(Packet::PACKET_TYPES[:set_clock_title], string.to_s)
end
def self.packet_get_clock_title
Packet.create(Packet::PACKET_TYPES[:get_clock_title], "")
end
def self.packet_clock_title(string)
Packet.create(Packet::PACKET_TYPES[:clock_title], string.to_s)
end
def self.packet_jukebox_previous_track
Packet.create(Packet::PACKET_TYPES[:jukebox_previous_track], "")
end
def self.packet_jukebox_next_track
Packet.create(Packet::PACKET_TYPES[:jukebox_next_track], "")
end
def self.packet_jukebox_play
Packet.create(Packet::PACKET_TYPES[:jukebox_play], "")
end
def self.packet_jukebox_pause
Packet.create(Packet::PACKET_TYPES[:jukebox_pause], "")
end
def self.packet_jukebox_stop
Packet.create(Packet::PACKET_TYPES[:jukebox_stop], "")
end
def self.packet_jukebox_set_volume(float)
Packet.create(Packet::PACKET_TYPES[:jukebox_set_volume], float.to_s)
end
def self.packet_jukebox_get_volume
Packet.create(Packet::PACKET_TYPES[:jukebox_get_volume], "")
end
def self.packet_jukebox_volume(float)
Packet.create(Packet::PACKET_TYPES[:jukebox_volume], float.to_s)
end
def self.packet_jukebox_set_sound_effects(boolean)
Packet.create(Packet::PACKET_TYPES[:jukebox_set_sound_effects], boolean.to_s)
end
def self.packet_jukebox_current_track(name)
Packet.create(Packet::PACKET_TYPES[:jukebox_current_track], name)
end
def self.packet_clock_time(string)
Packet.create(Packet::PACKET_TYPES[:clock_time], string)
end
def self.packet_randomizer_visible(boolean)
Packet.create(Packet::PACKET_TYPES[:randomizer_visible], boolean.to_s)
end
def self.packet_shutdown
Packet.create(Packet::PACKET_TYPES[:shutdown], "")
end
end
end
end
end

View File

@@ -0,0 +1,147 @@
module TAC
class PracticeGameClock
class ClockNet
class Server
TAG = "ClockNet|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,
:proxy_object
def initialize(hostname: "localhost", port: 4567, proxy_object: )
$server = self
@hostname = hostname
@port = port
@proxy_object = proxy_object
@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 = Gosu.milliseconds
@sync_interval = SYNC_INTERVAL
@last_heartbeat_sent = Gosu.milliseconds
@heartbeat_interval = HEARTBEAT_INTERVAL
@client_handler_proc = proc do
handle_client
end
@packet_handler = PacketHandler.new(proxy_object: @proxy_object)
end
def start(run_on_main_thread: false)
thread = Thread.new do
while (!@socket && @connection_attempts < @max_connection_attempts)
begin
log.i(TAG, "Starting server...")
@socket = TCPServer.new(@port)
@socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1)
rescue IOError => error
log.e(TAG, error)
@connection_attempts += 1
retry if @connection_attempts < @max_connection_attempts
end
end
while @socket && !@socket.closed?
begin
run_server
rescue IOError => error
log.e(TAG, error)
@socket.close if @socket
end
end
end
thread.join if run_on_main_thread
end
def run_server
while !@socket.closed?
client = Client.new
client.sync_interval = @sync_interval
client.socket = @socket.accept
if @active_client && @active_client.connected?
log.i(TAG, "Too many clients, already have one connected!")
client.close("Too many clients!")
else
@active_client = client
# TODO: Backup local config
# SEND CONFIG
@active_client.puts(PacketHandler.packet_handshake(@active_client.uuid))
log.i(TAG, "Client connected!")
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
if message && !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_heartbeat)
end
sleep @sync_interval / 1000.0
end
end
def close
@socket.close
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
end

View File

@@ -0,0 +1,135 @@
module TAC
class PracticeGameClock
class ParticleEmitter
def initialize(max_particles: 50, time_to_live: 30_000, interval: 1_500, z: -2)
@max_particles = max_particles
@time_to_live = time_to_live
@interval = interval
@z = -2
@particles = []
@image_options = Dir.glob("#{ROOT_PATH}/media/particles/*.*")
@last_spawned = 0
@clock_active = false
end
def draw
@particles.each(&:draw)
end
def update
@particles.each { |part| part.update($window.dt) }
@particles.delete_if { |part| part.die? }
spawn_particles
end
def spawn_particles
# !clock_active? &&
if @particles.count < @max_particles && Gosu.milliseconds - @last_spawned >= @interval
screen_midpoint = CyberarmEngine::Vector.new($window.width / 2, $window.height / 2)
scale = rand(0.25..1.0)
image_name = @image_options.sample
return unless image_name
image = $window.current_state.get_image(image_name)
position = CyberarmEngine::Vector.new(0, 0)
r = rand
if r < 0.25 # LEFT
position.x = -image.width * scale
position.y = rand(0..($window.height - image.height * scale))
elsif r < 0.5 # RIGHT
position.x = $window.width + (image.width * scale)
position.y = rand(0..($window.height - image.height * scale))
elsif r < 0.75 # TOP
position.x = rand(0..($window.width - image.width * scale))
position.y = -image.height * scale
else #BOTTOM
position.x = rand(0..($window.width - image.width * scale))
position.y = $window.height + image.height * scale
end
position.x ||= 0
position.y ||= 0
velocity = (screen_midpoint - position)
@particles << Particle.new(
image: image,
position: position,
velocity: velocity,
time_to_live: @time_to_live,
speed: rand(24..128),
scale: scale,
clock_active: @clock_active,
z: @z
)
@last_spawned = Gosu.milliseconds
end
end
def clock_active!
@clock_active = true
@particles.each(&:clock_active!)
end
def clock_inactive!
@clock_active = false
@particles.each(&:clock_inactive!)
end
def clock_active?
@clock_active
end
class Particle
def initialize(image:, position:, velocity:, time_to_live:, speed:, z:, scale: 1.0, clock_active: false)
@image = image
@position = position
@velocity = velocity.normalized
@time_to_live = time_to_live
@speed = speed
@z = z
@scale = scale
@born_at = Gosu.milliseconds
@born_time_to_live = time_to_live
@color = Gosu::Color.new(0xff_ffffff)
@clock_active = clock_active
end
def draw
@image.draw(@position.x, @position.y, @z, @scale, @scale, @color)
end
def update(dt)
@position += @velocity * @speed * dt
@color.alpha = (255.0 * ratio).to_i.clamp(0, 255)
end
def ratio
r = 1.0 - ((Gosu.milliseconds - @born_at) / @time_to_live.to_f)
@clock_active ? r.clamp(0.0, 0.5) : r
end
def die?
ratio <= 0
end
def clock_active!
@clock_active = true
# @time_to_live = (Gosu.milliseconds - @born_at) + 1_000
end
def clock_inactive!
@clock_active = false
# @time_to_live = @born_time_to_live unless Gosu.milliseconds - @born_at < @time_to_live
end
end
end
end
end

View File

@@ -0,0 +1,179 @@
require "securerandom"
module TAC
class PracticeGameClock
class Randomizer < CyberarmEngine::GameState
def setup
@roll = SecureRandom.random_number(1..6)
@dimple_color = 0xff_008000
@dimple_res = 36
@size = [window.width, window.height].min / 2.0
@ducks = []
case @roll
when 1, 4
# Blue: Right
# Red: Left
@ducks << Ducky.new(window: window, alliance: :blue, slot: 3, speed: 512, die_size: @size)
@ducks << Ducky.new(window: window, alliance: :red, slot: 1, speed: 512, die_size: @size)
when 2, 5
#Blue and Red: Center
@ducks << Ducky.new(window: window, alliance: :blue, slot: 2, speed: 512, die_size: @size)
@ducks << Ducky.new(window: window, alliance: :red, slot: 2, speed: 512, die_size: @size)
when 3, 6
# Blue: Left
# Red: Right
@ducks << Ducky.new(window: window, alliance: :blue, slot: 1, speed: 512, die_size: @size)
@ducks << Ducky.new(window: window, alliance: :red, slot: 3, speed: 512, die_size: @size)
end
end
def draw
window.previous_state.draw
Gosu.flush
fill(0xdd_202020)
Gosu.translate(window.width * 0.5 - @size * 0.5, 24) do
Gosu.draw_rect(0, 0, @size, @size, Gosu::Color::BLACK)
Gosu.draw_rect(12, 12, @size - 24, @size - 24, Gosu::Color::GRAY)
self.send(:"dice_#{@roll}", @size)
end
@ducks.each { |o| o.draw(@size) }
end
def dimple(size)
size / 9.0
end
def update
window.previous_state&.update_non_gui
@ducks.each { |o| o.update(@size) }
@size = [window.width, window.height].min / 2.0
end
def button_down(id)
case id
when Gosu::MS_LEFT, Gosu::KB_ESCAPE, Gosu::KB_SPACE
pop_state
end
end
def dice_1(size)
Gosu.draw_circle(size / 2, size / 2, dimple(size), @dimple_res, @dimple_color)
end
def dice_2(size)
Gosu.draw_circle(size * 0.25, size * 0.25, dimple(size), @dimple_res, @dimple_color)
Gosu.draw_circle(size * 0.75, size * 0.75, dimple(size), @dimple_res, @dimple_color)
end
def dice_3(size)
Gosu.draw_circle(size * 0.25, size * 0.25, dimple(size), @dimple_res, @dimple_color)
Gosu.draw_circle(size * 0.50, size * 0.50, dimple(size), @dimple_res, @dimple_color)
Gosu.draw_circle(size * 0.75, size * 0.75, dimple(size), @dimple_res, @dimple_color)
end
def dice_4(size)
Gosu.draw_circle(size * 0.25, size * 0.25, dimple(size), @dimple_res, @dimple_color)
Gosu.draw_circle(size * 0.75, size * 0.25, dimple(size), @dimple_res, @dimple_color)
Gosu.draw_circle(size * 0.25, size * 0.75, dimple(size), @dimple_res, @dimple_color)
Gosu.draw_circle(size * 0.75, size * 0.75, dimple(size), @dimple_res, @dimple_color)
end
def dice_5(size)
Gosu.draw_circle(size * 0.50, size * 0.50, dimple(size), @dimple_res, @dimple_color)
Gosu.draw_circle(size * 0.25, size * 0.25, dimple(size), @dimple_res, @dimple_color)
Gosu.draw_circle(size * 0.75, size * 0.25, dimple(size), @dimple_res, @dimple_color)
Gosu.draw_circle(size * 0.25, size * 0.75, dimple(size), @dimple_res, @dimple_color)
Gosu.draw_circle(size * 0.75, size * 0.75, dimple(size), @dimple_res, @dimple_color)
end
def dice_6(size)
Gosu.draw_circle(size * 0.25, size * 0.20, dimple(size), @dimple_res, @dimple_color)
Gosu.draw_circle(size * 0.75, size * 0.20, dimple(size), @dimple_res, @dimple_color)
Gosu.draw_circle(size * 0.25, size * 0.50, dimple(size), @dimple_res, @dimple_color)
Gosu.draw_circle(size * 0.75, size * 0.50, dimple(size), @dimple_res, @dimple_color)
Gosu.draw_circle(size * 0.25, size * 0.80, dimple(size), @dimple_res, @dimple_color)
Gosu.draw_circle(size * 0.75, size * 0.80, dimple(size), @dimple_res, @dimple_color)
end
class Ducky
SIZE = 0.20
HALF_SIZE = SIZE * 0.5
def initialize(window:, alliance:, slot:, speed:, die_size:)
@window = window
@alliance = alliance
@slot = slot
@speed = speed
@image = @window.get_image("#{ROOT_PATH}/media/openclipart_ducky.png")
@debug_text = Gosu::Font.new(28)
if @alliance == :blue
@position = CyberarmEngine::Vector.new(@window.width, die_size)
else
@position = CyberarmEngine::Vector.new(-die_size, die_size + die_size * 0.40)
end
end
def draw(size)
Gosu.translate(@position.x, @position.y) do
Gosu.draw_rect(0, size * SIZE, size * SIZE, size * SIZE, alliance_color)
Gosu.draw_rect(size * 0.5 - size * HALF_SIZE, size * SIZE, size * SIZE, size * SIZE, alliance_color)
Gosu.draw_rect(size * (1.0 - SIZE), size * SIZE, size * SIZE, size * SIZE, alliance_color)
duck_scale = (size * (SIZE + HALF_SIZE)) / @image.width
duck_scale_x = @alliance == :blue ? -duck_scale : duck_scale
@image.draw_rot(slot_position(size), size * SIZE + float_y(size), 1, 0, 0.5, 0.5, duck_scale_x, duck_scale)
end
end
def update(size)
center = @window.width * 0.5 - size * 0.5
if @position.x > center
@position.x -= @speed * @window.dt
@position.x = center if @position.x < center
elsif @position.x < center
@position.x += @speed * @window.dt
@position.x = center if @position.x > center
end
end
def alliance_color
@alliance == :blue ? 0xff_000080 : 0xff_800000
end
def slot_position(size)
case @slot
when 1
size * HALF_SIZE
when 2
size * 0.5
when 3
size * (1.0 - HALF_SIZE)
else
raise "Slot value of: #{@slot.inspect} is invalid!"
end
end
def float_y(size)
Math.sin(Gosu.milliseconds / 100.0) * (size * 0.01)
end
end
end
end
end

View File

@@ -0,0 +1,271 @@
module TAC
class PracticeGameClock
class RemoteControl
@@connection = nil
@@server = nil
def self.connection
@@connection
end
def self.connection=(connection)
@@connection = connection
end
def self.server
@@server
end
def self.server=(server)
@@server = server
end
class NetConnect < CyberarmEngine::GuiState
def setup
theme(THEME)
background Palette::TACNET_NOT_CONNECTED
banner "ClockNet Remote Control", text_align: :center, width: 1.0
flow(width: 1.0) do
stack(width: 0.25) {}
stack(width: 0.5) do
title "Hostname"
@hostname = edit_line "localhost", width: 1.0
title "Port"
@port = edit_line "4567", width: 1.0
flow(width: 1.0, margin_top: 20) do
@back_button = button "Back", width: 0.5 do
window.pop_state
end
@connect_button = button "Connect", width: 0.5 do
begin
@connect_button.enabled = false
@back_button.enabled = false
@connection = ClockNet::Connection.new(hostname: @hostname.value, port: Integer(@port.value), proxy_object: RemoteProxy.new(window))
@connection.connect
RemoteControl.connection = @connection
end
end
end
end
end
end
def update
super
if RemoteControl.connection
push_state(Controller) if RemoteControl.connection.connected?
RemoteControl.connection = nil if RemoteControl.connection.client.socket_error?
else
@back_button.enabled = true
@connect_button.enabled = true
end
end
end
class Controller < CyberarmEngine::GuiState
def setup
theme(THEME)
at_exit do
@connection&.close
end
@jukebox_volume = 1.0
@jukebox_sound_effects = true
@randomizer_visible = false
RemoteControl.connection.proxy_object.register(:track_changed, method(:track_changed))
RemoteControl.connection.proxy_object.register(:volume_changed, method(:volume_changed))
RemoteControl.connection.proxy_object.register(:clock_changed, method(:clock_changed))
RemoteControl.connection.proxy_object.register(:randomizer_changed, method(:randomizer_changed))
background Palette::TACNET_NOT_CONNECTED
banner "ClockNet Remote Control", text_align: :center, width: 1.0
flow width: 1.0, height: 1.0 do
stack width: 0.5 do
title "Match", width: 1.0, text_align: :center
button "Start Match", width: 1.0, text_size: 48, margin_bottom: 50 do
start_clock(:full_match)
end
title "Practice", width: 1.0, text_align: :center
button "Autonomous", width: 1.0 do
start_clock(:autonomous)
end
button "TeleOp with Countdown", width: 1.0 do
start_clock(:full_teleop)
end
button "TeleOp", width: 1.0 do
start_clock(:teleop_only)
end
button "TeleOp Endgame", width: 1.0, margin_bottom: 50 do
start_clock(:endgame_only)
end
button "Abort Match", width: 1.0 do
RemoteControl.connection.puts(ClockNet::PacketHandler.packet_abort_clock)
end
button "Shutdown", width: 1.0, **TAC::THEME_DANGER_BUTTON do
RemoteControl.connection.puts(ClockNet::PacketHandler.packet_shutdown)
sleep 1 # let packet escape before closing
end
end
stack width: 0.495 do
title "Clock Title", width: 1.0, text_align: :center
stack width: 0.9, margin_left: 50 do
@title = edit_line "FIRST TECH CHALLENGE", width: 1.0
button "Update", width: 1.0, margin_bottom: 50 do
RemoteControl.connection.puts(ClockNet::PacketHandler.packet_set_clock_title(@title.value.strip))
end
end
title "JukeBox", width: 1.0, text_align: :center
stack width: 0.9, margin_left: 50 do
flow width: 1.0 do
tagline "Now Playing: "
@track_name = tagline ""
end
flow width: 1.0 do
tagline "Volume: "
@volume = tagline "100%"
end
flow width: 1.0 do
button get_image("#{ROOT_PATH}/media/icons/previous.png") do
RemoteControl.connection.puts(ClockNet::PacketHandler.packet_jukebox_previous_track)
end
button get_image("#{ROOT_PATH}/media/icons/right.png") do |button|
if @jukebox_playing
RemoteControl.connection.puts(ClockNet::PacketHandler.packet_jukebox_pause)
button.value = get_image("#{ROOT_PATH}/media/icons/right.png")
@jukebox_playing = false
else
RemoteControl.connection.puts(ClockNet::PacketHandler.packet_jukebox_play)
button.value = get_image("#{ROOT_PATH}/media/icons/pause.png")
@jukebox_playing = true
end
end
button get_image("#{ROOT_PATH}/media/icons/stop.png") do
RemoteControl.connection.puts(ClockNet::PacketHandler.packet_jukebox_stop)
end
button get_image("#{ROOT_PATH}/media/icons/next.png") do
RemoteControl.connection.puts(ClockNet::PacketHandler.packet_jukebox_next_track)
end
button get_image("#{ROOT_PATH}/media/icons/minus.png"), margin_left: 20 do
@jukebox_volume -= 0.1
@jukebox_volume = 0.1 if @jukebox_volume < 0.1
RemoteControl.connection.puts(ClockNet::PacketHandler.packet_jukebox_set_volume(@jukebox_volume))
end
button get_image("#{ROOT_PATH}/media/icons/plus.png") do
@jukebox_volume += 0.1
@jukebox_volume = 1.0 if @jukebox_volume > 1.0
RemoteControl.connection.puts(ClockNet::PacketHandler.packet_jukebox_set_volume(@jukebox_volume))
end
button get_image("#{ROOT_PATH}/media/icons/musicOn.png"), margin_left: 20, tip: "Toggle Sound Effects" do |button|
if @jukebox_sound_effects
button.value = get_image("#{ROOT_PATH}/media/icons/musicOff.png")
@jukebox_sound_effects = false
else
button.value = get_image("#{ROOT_PATH}/media/icons/musicOn.png")
@jukebox_sound_effects = true
end
RemoteControl.connection.puts(ClockNet::PacketHandler.packet_jukebox_set_sound_effects(@jukebox_sound_effects))
end
end
button "Open Music Library", width: 1.0 do
path = "#{ROOT_PATH}/media/music"
if RUBY_PLATFORM.match(/ming|msys|cygwin/)
system("explorer \"#{path.gsub("/", "\\")}\"")
elsif RUBY_PLATFORM.match(/linux/)
system("xdg-open \"#{ROOT_PATH}/media/music\"")
else
# TODO.
end
end
end
stack width: 0.9, margin_left: 50, margin_top: 20 do
flow width: 1.0 do
title "Clock: "
@clock_label = title "0:00"
end
flow width: 1.0 do
title "Randomizer: "
@randomizer_label = title "Not Visible"
end
button "Randomizer", width: 1.0, **TAC::THEME_DANGER_BUTTON do
@randomizer_visible = !@randomizer_visible
RemoteControl.connection.puts(ClockNet::PacketHandler.packet_randomizer_visible(@randomizer_visible))
end
end
end
end
end
def update
super
while (o = RemoteControl.connection.proxy_object.queue.shift)
o.call
end
return if RemoteControl.connection.connected?
# We've lost connection, unset window's connection object
# and send user back to connect screen to to attempt to
# reconnect
RemoteControl.connection = nil
pop_state
end
def start_clock(mode)
RemoteControl.connection.puts(ClockNet::PacketHandler.packet_start_clock(mode.to_s))
end
def track_changed(name)
@track_name.value = name
end
def volume_changed(float)
@volume.value = "#{(float.round(1) * 100.0).round}%"
end
def clock_changed(string)
@clock_label.value = string
end
def randomizer_changed(boolean)
@randomizer_label.value = "Visible" if boolean
@randomizer_label.value = "Not Visible" unless boolean
end
end
end
end
end

View File

@@ -0,0 +1,71 @@
module TAC
class PracticeGameClock
class RemoteProxy
attr_reader :queue
def initialize(window)
@window = window
@queue = []
@callbacks = {}
end
def enqueue(&block)
@queue << block
end
def register(callback, method)
@callbacks[callback] = method
end
def start_clock(mode)
end
def abort_clock
end
def set_clock_title(string)
end
def get_clock_title(string)
end
def jukebox_previous_track
end
def jukebox_next_track
end
def jukebox_stop
end
def jukebox_play
end
def jukebox_pause
end
def jukebox_sound_effects(boolean)
end
def volume_changed(float)
@callbacks[:volume_changed]&.call(float)
end
def track_changed(name)
@callbacks[:track_changed]&.call(name)
end
def clock_changed(string)
@callbacks[:clock_changed]&.call(string)
end
def randomizer_changed(boolean)
@callbacks[:randomizer_changed]&.call(boolean)
end
def shutdown!
end
end
end
end

30
lib/game_clock/theme.rb Normal file
View File

@@ -0,0 +1,30 @@
module TAC
class PracticeGameClock
THEME = {
TextBlock: {
font: "Canterell",
color: Gosu::Color.new(0xee_ffffff)
},
Button: {
image_width: 40,
text_size: 40,
background: Palette::TIMECRAFTERS_PRIMARY,
border_thickness: 1,
border_color: Gosu::Color.new(0xff_111111),
hover: {
background: Palette::TIMECRAFTERS_SECONDARY,
},
active: {
background: Palette::TIMECRAFTERS_TERTIARY,
}
},
EditLine: {
caret_color: Gosu::Color.new(0xff_88ef90),
},
ToggleButton: {
width: 18,
checkmark_image: "#{File.expand_path("..", __dir__)}/media/icons/checkmark.png",
}
}
end
end

297
lib/game_clock/view.rb Normal file
View File

@@ -0,0 +1,297 @@
module TAC
class PracticeGameClock
class View < CyberarmEngine::GuiState
attr_reader :clock
def setup
@remote_control_mode = @options[:remote_control_mode]
window.show_cursor = !@remote_control_mode
@escape_counter = 0
@background_image = get_image("#{ROOT_PATH}/media/background.png")
# Preload duck image since Gosu and windows threads don't get along with OpenGL (image is blank if loaded in a threaded context)
get_image("#{ROOT_PATH}/media/openclipart_ducky.png")
@menu_background = 0xaa004000
@mouse = Mouse.new(window)
@clock = Clock.new
@clock.controller = nil
@last_clock_display_value = @clock.value
@particle_emitters = [
PracticeGameClock::ParticleEmitter.new
]
@last_clock_state = @clock.active?
theme(THEME)
@menu_container = flow width: 1.0 do
stack(width: 0.35, padding: 5) do
background @menu_background
title "Match", width: 1.0, text_align: :center
button "Start Match", width: 1.0, margin_bottom: 50 do
@clock_proxy.start_clock(:full_match)
end
title "Practice", width: 1.0, text_align: :center
button "Autonomous", width: 1.0 do
@clock_proxy.start_clock(:autonomous)
end
button "TeleOp with Countdown", width: 1.0 do
@clock_proxy.start_clock(:full_teleop)
end
button "TeleOp", width: 1.0 do
@clock_proxy.start_clock(:teleop_only)
end
button "TeleOp Endgame", width: 1.0 do
@clock_proxy.start_clock(:endgame_only)
end
button "Abort Match", width: 1.0, margin_top: 50 do
@clock_proxy.abort_clock
end
button "Close", width: 1.0, **TAC::THEME_DANGER_BUTTON do
if window.instance_variable_get(:"@states").size == 1
window.close
else
@server&.close
@jukebox.stop
window.fullscreen = false
window.pop_state
end
end
end
stack width: 0.4, padding_left: 50 do
background @menu_background
flow do
label "♫ Now playing:"
@current_song_label = label "♫ ♫ ♫"
end
flow do
label "Volume:"
@current_volume_label = label "100%"
end
flow do
button get_image("#{ROOT_PATH}/media/icons/previous.png") do
@jukebox.previous_track
end
button get_image("#{ROOT_PATH}/media/icons/pause.png") do |button|
if @jukebox.song && @jukebox.song.paused?
button.value = get_image("#{ROOT_PATH}/media/icons/right.png")
@jukebox.play
elsif !@jukebox.song
button.value = get_image("#{ROOT_PATH}/media/icons/right.png")
@jukebox.play
else
button.value = get_image("#{ROOT_PATH}/media/icons/pause.png")
@jukebox.pause
end
end
button get_image("#{ROOT_PATH}/media/icons/stop.png") do
@jukebox.stop
end
button get_image("#{ROOT_PATH}/media/icons/next.png") do
@jukebox.next_track
end
button get_image("#{ROOT_PATH}/media/icons/minus.png"), margin_left: 20 do
@jukebox.set_volume(@jukebox.volume - 0.1)
end
button get_image("#{ROOT_PATH}/media/icons/plus.png") do
@jukebox.set_volume(@jukebox.volume + 0.1)
end
button "Open Music Library", margin_left: 50 do
if RUBY_PLATFORM.match(/ming|msys|cygwin/)
system("explorer #{ROOT_PATH}/media/music")
elsif RUBY_PLATFORM.match(/linux/)
system("xdg-open #{ROOT_PATH}/media/music")
else
# TODO.
end
end
button get_image("#{ROOT_PATH}/media/icons/musicOn.png"), margin_left: 50, tip: "Toggle Sound Effects" do |button|
boolean = @jukebox.set_sfx(!@jukebox.play_sfx?)
if boolean
button.value = get_image("#{ROOT_PATH}/media/icons/musicOn.png")
else
button.value = get_image("#{ROOT_PATH}/media/icons/musicOff.png")
end
end
end
stack width: 1.0 do
button "Randomizer", width: 1.0, **TAC::THEME_DANGER_BUTTON do
unless @clock.active?
push_state(Randomizer)
end
end
end
end
end
@jukebox = Jukebox.new(@clock)
@clock_proxy = ClockProxy.new(@clock, @jukebox)
if @remote_control_mode
@server = ClockNet::Server.new(proxy_object: @clock_proxy)
@server.start
RemoteControl.server = @server
@clock_proxy.register(:randomizer_changed, method(:randomizer_changed))
end
end
def draw
background_image_scale = [window.width.to_f / @background_image.width, window.height.to_f / @background_image.height].max
@background_image.draw(0, 0, -3, background_image_scale, background_image_scale)
@particle_emitters.each(&:draw)
@clock.draw
super
end
def update
super
@clock.update
@mouse.update
update_non_gui
if @last_clock_state != @clock.active?
@particle_emitters.each { |emitter| @clock.active? ? emitter.clock_active! : emitter.clock_inactive! }
end
if @remote_control_mode
@menu_container.hide
else
if @mouse.last_moved < 1.5
@menu_container.show unless @menu_container.visible?
window.show_cursor = true
else
@menu_container.hide if @menu_container.visible?
window.show_cursor = false
end
end
if @clock.value != @last_clock_display_value
@last_clock_display_value = @clock.value
if @remote_control_mode && @server.active_client
@server.active_client.puts(ClockNet::PacketHandler.packet_clock_time(@last_clock_display_value))
end
end
if @last_track_name != @jukebox.current_track
track_changed(@jukebox.current_track)
end
if @last_volume != @jukebox.volume
volume_changed(@jukebox.volume)
end
@last_track_name = @jukebox.current_track
@last_volume = @jukebox.volume
@last_clock_state = @clock.active?
end
def update_non_gui
if @remote_control_mode
while (o = RemoteControl.server.proxy_object.queue.shift)
o.call
end
end
@particle_emitters.each(&:update)
@jukebox.update
end
def button_down(id)
super
@mouse.button_down(id)
case id
when Gosu::KB_ESCAPE
@escape_counter += 1
if @escape_counter >= 3
@server&.close
if window.instance_variable_get(:"@states").size == 1
window.close
else
window.fullscreen = false
window.pop_state
end
end
else
@escape_counter = 0
end
end
def track_changed(name)
@current_song_label.value = File.basename(name)
end
def volume_changed(float)
@current_volume_label.value = "#{(float.round(1) * 100.0).round}%"
end
def randomizer_changed(boolean)
if boolean
push_state(Randomizer) unless @clock.active?
else
pop_state if current_state.is_a?(Randomizer)
end
end
class Mouse
def initialize(window)
@window = window
@last_moved = 0
@last_position = CyberarmEngine::Vector.new(@window.mouse_x, @window.mouse_y)
end
def update
position = CyberarmEngine::Vector.new(@window.mouse_x, @window.mouse_y)
if @last_position != position
@last_position = position
@last_moved = Gosu.milliseconds
end
end
def button_down(id)
case id
when Gosu::MS_LEFT, Gosu::MS_MIDDLE, Gosu::MS_RIGHT
@last_moved = Gosu.milliseconds
end
end
def last_moved
(Gosu.milliseconds - @last_moved) / 1000.0
end
end
end
end
end

51
lib/page.rb Normal file
View File

@@ -0,0 +1,51 @@
module TAC
class Page
include CyberarmEngine::DSL
include CyberarmEngine::Common
attr_reader :menu_bar, :status_bar, :body
def initialize(host:)
@host = host
@header_bar_label = host.header_bar_label
@menu_bar = host.menu_bar
@status_bar = host.status_bar
@body = host.body
@options = {}
end
def options=(options)
@options = options
end
def page(klass, options = {})
@host.page(klass, options)
end
def header_bar(text)
@header_bar_label.value = text
end
def setup
end
def focus
end
def blur
end
def draw
end
def update
end
def button_down(id)
end
def button_up(id)
end
end
end

View File

@@ -1,44 +1,27 @@
module TAC
class States
class ManageConfigurations < CyberarmEngine::GuiState
class Pages
class Configurations < Page
def setup
theme(THEME)
stack width: 1.0, height: 0.1 do
background THEME_HEADER_BACKGROUND
label "#{TAC::NAME} ― Manage Configurations", bold: true, text_size: THEME_HEADING_TEXT_SIZE
flow do
button "Close" do
if window.backend.settings.config
window.backend.load_config(window.backend.settings.config)
header_bar("Manage Configurations")
pop_state
menu_bar.clear do
button get_image("#{TAC::ROOT_PATH}/media/icons/plus.png"), image_height: 1.0, tip: "Add configuration" do
push_state(Dialog::NamePromptDialog, title: "Config Name", callback_method: proc { |name|
window.backend.write_new_config(name)
window.current_state.refresh_config
else
push_state(Dialog::AlertDialog, title: "No Config Loaded", message: "A config must be loaded.")
end
end
label "Current Configuration: "
@config_label = label window.backend.settings.config
change_config(name)
populate_configs
})
end
end
stack width: 1.0, height: 0.9 do
background THEME_CONTENT_BACKGROUND
flow do
label "Configurations", text_size: THEME_SUBHEADING_TEXT_SIZE
button get_image("#{TAC::ROOT_PATH}/media/icons/plus.png"), image_width: 18, tip: "Add configuration" do
push_state(Dialog::NamePromptDialog, title: "Config Name", callback_method: proc { |name|
window.backend.write_new_config(name)
status_bar.clear do
label "Current Configuration: "
@config_label = label window.backend.settings.config
end
change_config(name)
populate_configs
})
end
end
@configs_list = stack width: 1.0 do
body.clear do
@configs_list = stack width: 1.0, height: 1.0, scroll: true do
end
end
@@ -58,6 +41,10 @@ module TAC
button "#{name}", width: 0.94 do
change_config(name)
if window.backend.tacnet.connected?
window.backend.tacnet.puts(TAC::TACNET::PacketHandler.packet_select_config(name))
end
end
button get_image("#{TAC::ROOT_PATH}/media/icons/gear.png"), image_width: THEME_ICON_SIZE, tip: "Rename configuration" do
@@ -74,17 +61,21 @@ module TAC
populate_configs
else
push_state(Dialog::AlertDialog, title: "Config Rename Failed", message: "File already exists at\n#{TAC::CONFIGS_PATH}/#{new_name}.json}")
push_state(Dialog::AlertDialog, title: "Config Rename Failed", message: "File already exists at #{TAC::CONFIGS_PATH}/#{new_name}.json}")
end
})
end
button get_image("#{TAC::ROOT_PATH}/media/icons/trashcan.png"), image_width: THEME_ICON_SIZE, **THEME_DANGER_BUTTON, tip: "Delete configuration" do
push_state(Dialog::ConfirmDialog, title: "Delete Config?", callback_method: proc {
push_state(Dialog::ConfirmDialog, title: "Delete Config?", dangerous: true, callback_method: proc {
File.delete("#{TAC::CONFIGS_PATH}/#{name}.json")
if window.backend.settings.config == name
change_config(nil)
change_config("")
end
if window.backend.tacnet.connected?
window.backend.tacnet.puts(TAC::TACNET::PacketHandler.packet_delete_config(name))
end
populate_configs
@@ -98,6 +89,7 @@ module TAC
def change_config(name)
window.backend.settings.config = name
window.backend.save_settings
window.backend.load_config(name)
@config_label.value = name.to_s
end

View File

@@ -0,0 +1,204 @@
module TAC
class Pages
class DriveTeamRotationGenerator < Page
FILENAME = "#{TAC::ROOT_PATH}/data/drive_team_rotation.csv"
def setup
header_bar("Drive Team Rotation Generator")
@roster ||= [
"Alexander",
"Aubrey",
"Cayden",
"Gabe",
"Spencer",
"Olivia"
]
@roles ||= [
"Coach",
"Driver A",
"Driver B"
]
menu_bar.clear do
button get_image("#{TAC::ROOT_PATH}/media/icons/save.png"), image_height: 1.0, tip: "Export rotation as Comma-Seperated Values" do
export_rotation
@status_bar.clear do
tagline "Saved to: #{FILENAME}"
end
end
button get_image("#{TAC::ROOT_PATH}/media/icons/target.png"), margin_right: 10, image_height: 1.0, tip: "Generate rotation" do
populate_rotation
end
end
body.clear do
flow(margin_left: 20, width: 1.0, height: 1.0) do
stack(width: 0.25) do
title "Roles", width: 1.0, margin_bottom: 4, text_align: :center
flow(width: 1.0, margin_bottom: 20) do
@role_name = edit_line "", placeholder: "Add role", width: 0.9
button get_image("#{TAC::ROOT_PATH}/media/icons/plus.png"), image_width: 0.1, tip: "Add role" do
if @role_name.value.strip.length.positive?
@roles.push(@role_name.value.strip)
@role_name.value = ""
populate_roles
end
end
end
@roles_container = stack(width: 1.0, height: 0.835, scroll: true) do
end
end
stack(margin_left: 20, width: 0.25, height: 1.0) do
title "Roster", width: 1.0, margin_bottom: 4, text_align: :center
flow(width: 1.0, margin_bottom: 20) do
@roster_name = edit_line "", placeholder: "Add name", width: 0.9
button get_image("#{TAC::ROOT_PATH}/media/icons/plus.png"), image_width: 0.1, tip: "Add name" do
if @roster_name.value.strip.length.positive?
@roster.push(@roster_name.value.strip)
@roster_name.value = ""
populate_roster
end
end
end
@roster_container = stack(width: 1.0, height: 0.835, scroll: true) do
end
end
stack(margin_left: 20, margin_right: 20, width: 0.5, height: 1.0) do
title "Rotation", width: 1.0, margin_bottom: 4, text_align: :center
@rotation_container = stack(width: 1.0, height: 0.835, scroll: true) do
end
end
end
end
populate_roles
populate_roster
populate_rotation
end
def populate_roles
@roles_container.clear do
@roles.each_with_index do |name, i|
flow(width: 1.0, padding: 2) do
background i.even? ? 0xff_007000 : 0xff_006000
tagline name, width: 0.9
button get_image("#{TAC::ROOT_PATH}/media/icons/trashcan.png"), image_width: 0.1, tip: "Remove role", **THEME_DANGER_BUTTON do
@roles.delete(name)
populate_roles
end
end
end
end
end
def populate_roster
@roster_container.clear do
@roster.each_with_index do |name, i|
flow(width: 1.0, padding: 2) do
background i.even? ? 0xff_007000 : 0xff_006000
tagline name, width: 0.9
button get_image("#{TAC::ROOT_PATH}/media/icons/trashcan.png"), image_width: 0.1, tip: "Remove name", **THEME_DANGER_BUTTON do
@roster.delete(name)
populate_roster
end
end
end
end
end
def populate_rotation
@rotation = Generator.new(roster: @roster, team_size: @roles.count)
@rotation_container.clear do
fraction = (1.0 / @rotation.team_size) - 0.02
flow(width: 1.0, padding: 2) do
background Gosu::Color::BLACK
@roles.each do |role|
tagline "<b>#{role}</b>", width: fraction
end
end
teams = @rotation.teams unless @shuffle_teams&.value
teams = @rotation.teams.shuffle if @shuffle_teams&.value
teams.each_with_index do |team, i|
flow(width: 1.0, padding: 2) do
background i.even? ? 0xff_007000 : 0xff_006000
team.each do |player|
tagline player, width: fraction
end
end
end
end
end
def export_rotation
return unless @rotation
buff = "#{@roles.join(',')}\n"
@rotation.teams.each do |team|
buff += "#{team.join(",")}\n"
end
buff.strip
File.write(FILENAME, buff)
end
end
class Generator
attr_reader :roster, :team_size, :rounds, :teams, :schedule
def initialize(roster:, team_size: 4, rounds: 6)
@roster = roster.clone
@roster.freeze
@team_size = team_size
@rounds = rounds
@teams = []
@schedule = []
generate
end
def generate
generate_teams
generate_round_robin
end
def generate_teams
return unless @roster.size >= @team_size
list = @roster.dup
list.size.times do
list.rotate!
@teams << list[0..@team_size - 1]
end
end
def generate_round_robin
end
end
end
end

420
lib/pages/editor.rb Normal file
View File

@@ -0,0 +1,420 @@
module TAC
class Pages
class Editor < Page
def setup
header_bar("Editor")
@active_group = nil
@active_action = nil
menu_bar.clear do
if @options[:group_is_preset]
title "Editing group preset: #{@options[:group].name}"
elsif @options[:action_is_preset]
title "Editing action preset: #{@options[:action].name}"
else
title "Editing configuration: #{window.backend.config.name}"
end
end
status_bar.clear do
flow(width: 0.3333) do
label "Active group:", margin_right: 20
@active_group_label = label ""
end
flow(width: 0.3333) do
label "Active action:", margin_right: 20
@active_action_label = label ""
end
end
body.clear do
flow(width: 1.0, height: 1.0) do
stack width: 0.33333, height: 1.0, border_thickness_right: 1, border_color: [0, Gosu::Color::BLACK, 0, 0] do
@groups_menu = flow(width: 1.0) do
label "Groups", text_size: THEME_SUBHEADING_TEXT_SIZE
button get_image("#{TAC::ROOT_PATH}/media/icons/plus.png"), image_width: THEME_ICON_SIZE, tip: "Add group" do
push_state(TAC::Dialog::NamePromptDialog, title: "Create Group", list: window.backend.config.groups, callback_method: method(:create_group))
end
button get_image("#{TAC::ROOT_PATH}/media/icons/button2.png"), image_width: THEME_ICON_SIZE, tip: "Clone currently selected group" do
if @active_group
push_state(Dialog::NamePromptDialog, title: "Clone Group", renaming: @active_group, accept_label: "Clone", list: window.backend.config.groups, callback_method: proc { |group, name|
clone = TAC::Config::Group.from_json( JSON.parse( @active_group.to_json, symbolize_names: true ))
clone.name = "#{name}"
window.backend.config.groups << clone
window.backend.config_changed!
populate_groups_list
})
else
push_state(TAC::Dialog::AlertDialog, title: "Error", message: "Unable to clone group, no group selected.")
end
end
button get_image("#{TAC::ROOT_PATH}/media/icons/save.png"), image_width: THEME_ICON_SIZE, tip: "Save group as preset" do
if @active_group
push_state(Dialog::NamePromptDialog, title: "Save Group Preset", renaming: @active_group, accept_label: "Save", list: window.backend.config.presets.groups, callback_method: proc { |group, name|
clone = TAC::Config::Group.from_json( JSON.parse( @active_group.to_json, symbolize_names: true ))
clone.name = "#{name}"
window.backend.config.presets.groups << clone
window.backend.config.presets.groups.sort_by! { |g| g.name.downcase }
window.backend.config_changed!
window.toast("Saved Group Preset", "Saved preset: #{name}")
})
else
push_state(TAC::Dialog::AlertDialog, title: "Error", message: "Unable to create group preset, no group selected.")
end
end
button get_image("#{TAC::ROOT_PATH}/media/icons/import.png"), image_width: THEME_ICON_SIZE, tip: "Import group from preset" do
push_state(Dialog::PickPresetDialog, title: "Pick Group Preset", limit: :groups, callback_method: proc { |preset|
push_state(Dialog::NamePromptDialog, title: "Name Group", renaming: preset, accept_label: "Add", list: window.backend.config.groups, callback_method: proc { |group, name|
clone = TAC::Config::Group.from_json( JSON.parse( group.to_json, symbolize_names: true ))
clone.name = "#{name}"
window.backend.config.groups << clone
window.backend.config.groups.sort_by! { |g| g.name.downcase }
window.backend.config_changed!
populate_groups_list
})
})
end
end
@groups_list = stack width: 1.0, scroll: true do
end
end
stack width: 0.33333, height: 1.0, border_thickness_right: 1, border_color: [0, Gosu::Color::BLACK, 0, 0] do
@actions_menu = flow(width: 1.0) do
label "Actions", text_size: THEME_SUBHEADING_TEXT_SIZE
button get_image("#{TAC::ROOT_PATH}/media/icons/plus.png"), image_width: THEME_ICON_SIZE, tip: "Add action" do
if @active_group
push_state(TAC::Dialog::ActionDialog, title: "Create Action", list: @active_group.actions, callback_method: method(:create_action))
else
push_state(TAC::Dialog::AlertDialog, title: "Error", message: "Unable to create action, no group selected.")
end
end
button get_image("#{TAC::ROOT_PATH}/media/icons/button2.png"), image_width: THEME_ICON_SIZE, tip: "Clone currently selected action" do
if @active_group && @active_action
push_state(Dialog::ActionDialog, title: "Clone Action", action: @active_action, cloning: true, accept_label: "Clone", list: @active_group.actions, callback_method: proc { |action, name, comment|
clone = TAC::Config::Action.from_json( JSON.parse( @active_action.to_json, symbolize_names: true ))
clone.name = name
clone.comment = comment
@active_group.actions << clone
window.backend.config_changed!
populate_actions_list(@active_group)
})
else
push_state(TAC::Dialog::AlertDialog, title: "Error", message: "Unable to clone action, no action selected.")
end
end
button get_image("#{TAC::ROOT_PATH}/media/icons/save.png"), image_width: THEME_ICON_SIZE, tip: "Save action as preset" do
if @active_action
push_state(Dialog::NamePromptDialog, title: "Save Action Preset", renaming: @active_action, accept_label: "Save", list: window.backend.config.presets.actions, callback_method: proc { |action, name|
clone = TAC::Config::Action.from_json( JSON.parse( @active_action.to_json, symbolize_names: true ))
clone.name = "#{name}"
window.backend.config.presets.actions << clone
window.backend.config.presets.actions.sort_by! { |a| a.name.downcase }
window.backend.config_changed!
window.toast("Saved Action Preset", "Saved preset: #{name}")
})
else
push_state(TAC::Dialog::AlertDialog, title: "Error", message: "Unable to create action preset, no action selected.")
end
end
button get_image("#{TAC::ROOT_PATH}/media/icons/import.png"), image_width: THEME_ICON_SIZE, tip: "Import action from preset" do
if @active_group
push_state(Dialog::PickPresetDialog, title: "Pick Action Preset", limit: :actions, callback_method: proc { |preset|
push_state(Dialog::ActionDialog, title: "Name Action", action: preset, accept_label: "Add", list: @active_group.actions, callback_method: proc { |action, name|
clone = TAC::Config::Action.from_json( JSON.parse( action.to_json, symbolize_names: true ))
clone.name = "#{name}"
@active_group.actions << clone
@active_group.actions.sort_by! { |a| a.name.downcase }
window.backend.config_changed!
populate_actions_list(@active_group)
})
})
else
push_state(TAC::Dialog::AlertDialog, title: "Error", message: "Unable to import action preset, no group selected.")
end
end
end
@actions_list = stack width: 1.0, scroll: true do
end
end
stack width: 0.331, height: 1.0 do
@variables_menu = flow(width: 1.0) do
label "Variables", text_size: THEME_SUBHEADING_TEXT_SIZE
button get_image("#{TAC::ROOT_PATH}/media/icons/plus.png"), image_width: THEME_ICON_SIZE, tip: "Add variable" do
if @active_action
push_state(TAC::Dialog::VariableDialog, title: "Create Variable", callback_method: method(:create_variable))
else
push_state(TAC::Dialog::AlertDialog, title: "Error", message: "Unable to create variable, no action selected.")
end
end
end
@variables_list = stack width: 1.0, scroll: true do
end
end
end
end
populate_groups_list
if @options[:group_is_preset]
@active_group = @options[:group]
@active_group_label.value = @active_group.name
populate_actions_list(@active_group)
@groups_menu.hide
elsif @options[:action_is_preset]
@active_action = @options[:action]
@active_action_label.value = @active_action.name
populate_variables_list(@options[:action])
@groups_menu.hide
@actions_menu.hide
else
if @options[:group]
@active_group = @options[:group]
@active_group_label.value = @active_group.name
populate_actions_list(@active_group)
if @options[:action]
@active_action = @options[:action]
@active_action_label.value = @active_action.name
populate_variables_list(@active_action)
if @options[:variable]
# Scroll into view?
end
end
end
end
body.root.subscribe(:window_size_changed) do
set_list_heights
end
end
def set_list_heights
@groups_list.style.height = body.height - @groups_menu.height
@actions_list.style.height = body.height - @actions_menu.height
@variables_list.style.height = body.height - @variables_menu.height
end
def create_group(name)
window.backend.config.groups << TAC::Config::Group.new(name: name, actions: [])
window.backend.config.groups.sort_by! { |g| g.name.downcase }
window.backend.config_changed!
populate_groups_list
end
def update_group(group, name)
group.name = name
window.backend.config.groups.sort_by! { |g| g.name.downcase }
window.backend.config_changed!
populate_groups_list
end
def delete_group(group)
window.backend.config.groups.delete(group)
window.backend.config.groups.sort_by! { |g| g.name.downcase }
window.backend.config_changed!
@active_group = nil
@active_group_label.value = ""
@active_action = nil
@active_action_label.value = ""
@actions_list.clear
@variables_list.clear
populate_groups_list
end
def create_action(name, comment)
@active_group.actions << TAC::Config::Action.new(name: name, comment: comment, enabled: true, variables: [])
@active_group.actions.sort_by! { |a| a.name.downcase }
window.backend.config_changed!
populate_actions_list(@active_group)
end
def update_action(action, name, comment)
action.name = name
action.comment = comment
window.backend.config_changed!
populate_actions_list(@active_group)
end
def delete_action(action)
@active_group.actions.delete(action)
@active_group.actions.sort_by! { |a| a.name.downcase }
window.backend.config_changed!
@active_action = nil
@active_action_label.value = ""
@variables_list.clear
populate_actions_list(@active_group)
end
def create_variable(name, type, value)
@active_action.variables << TAC::Config::Variable.new(name: name, type: type, value: value)
@active_action.variables.sort_by! { |v| v.name.downcase }
window.backend.config_changed!
populate_variables_list(@active_action)
end
def update_variable(variable, name, type, value)
variable.name = name
variable.type = type
variable.value = value
@active_action.variables.sort_by! { |v| v.name.downcase }
window.backend.config_changed!
populate_variables_list(@active_action)
end
def delete_variable(variable)
@active_action.variables.delete(variable)
@active_action.variables.sort_by! { |v| v.name.downcase }
window.backend.config_changed!
populate_variables_list(@active_action)
end
def populate_groups_list
@groups_list.scroll_top = 0
groups = []
unless @options[:group_is_preset] || @options[:action_is_preset]
groups = window.backend.config.groups
end
@groups_list.clear do
groups.each_with_index do |group, i|
flow width: 1.0, **THEME_ITEM_CONTAINER_PADDING do
background i.even? ? THEME_EVEN_COLOR : THEME_ODD_COLOR
button group.name, width: 0.8 do
@active_group = group
@active_group_label.value = group.name
@active_action = nil
@active_action_label.value = ""
populate_actions_list(group)
@variables_list.clear
end
button get_image("#{TAC::ROOT_PATH}/media/icons/gear.png"), image_width: THEME_ICON_SIZE, tip: "Edit group" do
push_state(Dialog::NamePromptDialog, title: "Rename Group", renaming: group, list: window.backend.config.groups, callback_method: method(:update_group))
end
button get_image("#{TAC::ROOT_PATH}/media/icons/trashcan.png"), image_width: THEME_ICON_SIZE, tip: "Delete group", **THEME_DANGER_BUTTON do
push_state(Dialog::ConfirmDialog, dangerous: true, title: "Are you sure?", message: "Delete group and all of its actions and variables?", callback_method: proc { delete_group(group) })
end
end
end
end
set_list_heights
end
def populate_actions_list(group)
@actions_list.scroll_top = 0
actions = group.actions
@actions_list.clear do
actions.each_with_index do |action, i|
stack width: 1.0, **THEME_ITEM_CONTAINER_PADDING do
background i.even? ? THEME_EVEN_COLOR : THEME_ODD_COLOR
flow width: 1.0 do
button action.name, width: 0.72 do
@active_action = action
@active_action_label.value = action.name
populate_variables_list(action)
end
action_enabled_toggle = toggle_button tip: "Enable action", checked: action.enabled
action_enabled_toggle.subscribe(:changed) do |sender, value|
action.enabled = value
window.backend.config_changed!
end
button get_image("#{TAC::ROOT_PATH}/media/icons/gear.png"), image_width: THEME_ICON_SIZE, tip: "Edit action" do
push_state(Dialog::ActionDialog, title: "Rename Action", action: action, list: @active_group.actions, callback_method: method(:update_action))
end
button get_image("#{TAC::ROOT_PATH}/media/icons/trashcan.png"), image_width: THEME_ICON_SIZE, tip: "Delete action", **THEME_DANGER_BUTTON do
push_state(Dialog::ConfirmDialog, dangerous: true, title: "Are you sure?", message: "Delete action and all of its variables?", callback_method: proc { delete_action(action) })
end
end
caption "#{action.comment}", width: 1.0, text_wrap: :word_wrap unless action.comment.empty?
end
end
end
set_list_heights
end
def populate_variables_list(action)
@variables_list.scroll_top = 0
variables = action.variables
@variables_list.clear do
variables.each_with_index do |variable, i|
stack width: 1.0, **THEME_ITEM_CONTAINER_PADDING do
background i.even? ? THEME_EVEN_COLOR : THEME_ODD_COLOR
flow(width: 1.0) do
button "#{variable.name}", width: 0.89, tip: "Edit variable" do
push_state(Dialog::VariableDialog, title: "Edit Variable", variable: variable, callback_method: method(:update_variable))
end
button get_image("#{TAC::ROOT_PATH}/media/icons/trashcan.png"), image_width: THEME_ICON_SIZE, tip: "Delete variable", **THEME_DANGER_BUTTON do
push_state(Dialog::ConfirmDialog, title: "Are you sure?", message: "Delete variable?", callback_method: proc { delete_variable(variable) })
end
end
caption "Type: #{variable.type}"
caption "Value: #{variable.value}"
end
end
end
set_list_heights
end
end
end
end

226
lib/pages/field_planner.rb Normal file
View File

@@ -0,0 +1,226 @@
module TAC
class Pages
class FieldPlanner < Page
def setup
header_bar("Field Planner")
menu_bar.clear do
flow(width: 1.0, height: 1.0) do
button "Inches", text_size: THEME_HEADING_TEXT_SIZE do
@unit = :inches
refresh_panel
end
button "Feet", text_size: THEME_HEADING_TEXT_SIZE do
@unit = :feet
refresh_panel
end
button "Millimeters", text_size: THEME_HEADING_TEXT_SIZE do
@unit = :millimeters
refresh_panel
end
button "Centimeters", text_size: THEME_HEADING_TEXT_SIZE do
@unit = :centimeters
refresh_panel
end
button "Meters", text_size: THEME_HEADING_TEXT_SIZE do
@unit = :meters
refresh_panel
end
button "Reset", text_size: THEME_HEADING_TEXT_SIZE, **THEME_DANGER_BUTTON do
@nodes.clear
refresh_panel
end
end
end
status_bar.clear do
flow(width: 1.0, height: 1.0) do
tagline "Nodes:"
@nodes_count_label = tagline "0"
tagline "Total Distance:"
@total_distance_label = tagline "0"
@units_label = tagline "Inches"
end
end
body.clear do
flow(width: 1.0, height: 1.0) do
@field_container = stack width: 0.5, height: 1.0 do
background 0xff_111111
end
@points_container = stack width: 0.5, height: 1.0 do
end
end
end
@field = TAC::Simulator::Field.new(container: @field_container, season: :freight_frenzy, simulation: nil)
@nodes ||= []
@unit = :inches
@total_distance = 0
@node_color = 0xff_00f000
@node_hover_color = Gosu::Color::YELLOW
@segment_color = 0xaa_00f000
@node_radius = 6
@segment_thickness = 2
measure_path
refresh_panel
end
def draw
super
@field.draw
display_path
end
def update
super
@field.update
measure_path
end
def button_down(id)
super
if @field_container.hit?(window.mouse_x, window.mouse_y)
x = (window.mouse_x - @field_container.x) / @field.scale
y = (window.mouse_y - @field_container.y) / @field.scale
case id
when Gosu::MS_LEFT # Add Node
@nodes << CyberarmEngine::Vector.new(x, y, 0)
measure_path
refresh_panel
when Gosu::MS_RIGHT # Delete Node
result = @nodes.find do |node|
Gosu.distance(node.x, node.y, x, y) <= @node_radius * 0.25
end
@nodes.delete(result) if result
measure_path
refresh_panel
end
end
end
def display_path
x = (window.mouse_x - @field_container.x) / @field.scale
y = (window.mouse_y - @field_container.y) / @field.scale
last_node = @nodes.first
@nodes.each_with_index do |current_node, i|
mouse_near = Gosu.distance(current_node.x, current_node.y, x, y) <= @node_radius * 0.25
Gosu.draw_circle(
current_node.x * @field.scale + @field_container.x,
current_node.y * @field.scale + @field_container.y,
@node_radius, 7, mouse_near ? @node_hover_color : @node_color, 10
)
next if i.zero?
angle = Gosu.angle(
last_node.x * @field.scale,
last_node.y * @field.scale,
current_node.x * @field.scale,
current_node.y * @field.scale
)
distance = Gosu.distance(last_node.x, last_node.y, current_node.x, current_node.y) * @field.scale
Gosu.rotate(angle, last_node.x * @field.scale + @field_container.x, last_node.y * @field.scale + @field_container.y) do
Gosu.draw_rect(
(@field_container.x + last_node.x * @field.scale) - (@segment_thickness / 2.0),
(@field_container.y + last_node.y * @field.scale) - distance,
@segment_thickness,
distance,
@segment_color
)
end
last_node = current_node
end
end
def measure_path
@total_distance = 0
v1 = @nodes.first
@nodes.each_with_index do |v2, i|
next if i.zero?
@total_distance += Gosu.distance(
v1.x + @field_container.x,
v1.y + @field_container.y,
v2.x + @field_container.x,
v2.y + @field_container.y
)
v1 = v2
end
end
def refresh_panel
@nodes_count_label.value = "#{@nodes.count}"
@total_distance_label.value = "#{inches_to_unit(@total_distance).round(2)}"
@units_label.value = @unit.to_s.capitalize
# @points_container.clear do
# v1 = @nodes.first
# break unless v1
# para "Vector #{inches_to_unit(v1.x).round}:#{inches_to_unit(v1.y).round} - 0 #{@unit.to_s.capitalize}"
# @nodes.each_with_index do |v2, i|
# next if i.zero?
# distance = Gosu.distance(
# v1.x + @field_container.x,
# v1.y + @field_container.y,
# v2.x + @field_container.x,
# v2.y + @field_container.y
# )
# para "Vector #{inches_to_unit(v1.x).round}:#{inches_to_unit(v1.y).round} - #{inches_to_unit(distance).round(2)} #{@unit.to_s.capitalize}"
# v1 = v2
# end
# end
end
def inches_to_unit(inches)
case @unit
when :inches
inches
when :feet
inches / 12.0
when :millimeters
inches / 0.254
when :centimeters
inches / 2.54
when :meters
inches / 25.4
end
end
end
end
end

56
lib/pages/game_clock.rb Normal file
View File

@@ -0,0 +1,56 @@
module TAC
class Pages
class GameClock < Page
def setup
header_bar("Game Clock")
body.clear do
flow(width: 1.0, height: 1.0) do
@command_options = flow(width: 1.0) do
stack(width: 0.3) do
end
stack(width: 0.4) do
banner "Choose Mode", width: 1.0, text_align: :center
title "Local", width: 1.0, text_align: :center
button "Game Clock", width: 1.0 do
push_state(PracticeGameClock::View)
window.fullscreen = true
end
button "Dual Screen Game Clock", width: 1.0 do
# Spawn game clock window
$clock_pid = Process.spawn(
RbConfig.ruby,
"#{ROOT_PATH}/timecrafters_configuration_tool.rb",
"--game-clock-remote-display"
)
# switch to remote control
push_state(PracticeGameClock::RemoteControl::NetConnect)
end
title "Remote", width: 1.0, text_align: :center, margin_top: 32
button "Game Clock Display", width: 1.0 do
push_state(PracticeGameClock::View, remote_control_mode: true)
window.fullscreen = true
end
button "Game Clock Remote Control", width: 1.0 do
push_state(PracticeGameClock::RemoteControl::NetConnect)
end
end
stack(width: 0.3) do
end
end
end
end
end
end
end
end

42
lib/pages/home.rb Normal file
View File

@@ -0,0 +1,42 @@
module TAC
class Pages
class Home < Page
def setup
header_bar(TAC::NAME)
body.clear do
stack(width: 1.0, height: 1.0) do
label TAC::NAME, width: 1.0, text_size: 48, text_align: :center
stack(width: 1.0, height: 8) do
background 0xff_006000
end
if window.backend.settings.config.empty?
label "TODO: Introduction"
label "Get Started", text_size: 28
button "1. Create a configuration" do
page(TAC::Pages::Configurations)
end
label "2. Add a group"
label "3. Add an action"
label "4. Add a variable"
label "5. Profit?"
else
label "Display config stats or something?"
config = window.backend.config
groups = config.groups
actions = config.groups.map { |g| g.actions }.flatten
variables = actions.map { |a| a.variables }.flatten
label "Total groups: #{groups.size}"
label "Total actions: #{actions.size}"
label "Total variables: #{variables.size}"
end
end
end
end
end
end
end

125
lib/pages/presets.rb Normal file
View File

@@ -0,0 +1,125 @@
module TAC
class Pages
class Presets < Page
def setup
header_bar("Manage Presets")
status_bar.clear do
tagline "Group Presets", width: 0.495
tagline "Action Presets", width: 0.495
end
body.clear do
flow(width: 1.0, height: 1.0) do
@group_presets = stack(width: 0.49995, height: 1.0, scroll: true, border_thickness_right: 1, border_color: [0, Gosu::Color::BLACK, 0, 0]) do
end
@action_presets = stack(width: 0.49995, height: 1.0, scroll: true) do
end
end
end
populate_group_presets
populate_action_presets
end
def populate_group_presets
@group_presets.clear do
window.backend.config.presets.groups.each_with_index do |group, i|
flow(width: 1.0, **THEME_ITEM_CONTAINER_PADDING) do
background i.even? ? THEME_EVEN_COLOR : THEME_ODD_COLOR
button group.name, width: 0.895 do
page(TAC::Pages::Editor,{ group: group, group_is_preset: true })
end
button get_image("#{TAC::ROOT_PATH}/media/icons/gear.png"), image_width: THEME_ICON_SIZE, tip: "Edit group preset" do
push_state(
Dialog::NamePromptDialog,
title: "Rename Group Preset",
renaming: group,
list: window.backend.config.presets.groups,
callback_method: method(:update_group_preset)
)
end
button get_image("#{TAC::ROOT_PATH}/media/icons/trashcan.png"), image_width: THEME_ICON_SIZE, tip: "Delete group preset", **THEME_DANGER_BUTTON do
push_state(
Dialog::ConfirmDialog,
title: "Are you sure?",
message: "Delete group preset and all of its actions and variables?",
callback_method: proc { delete_group_preset(group) }
)
end
end
end
end
end
def populate_action_presets
@action_presets.clear do
window.backend.config.presets.actions.each_with_index do |action, i|
flow(width: 1.0, **THEME_ITEM_CONTAINER_PADDING) do
background i.even? ? THEME_EVEN_COLOR : THEME_ODD_COLOR
button action.name, width: 0.895 do
page(TAC::Pages::Editor,{ action: action, action_is_preset: true })
end
button get_image("#{TAC::ROOT_PATH}/media/icons/gear.png"), image_width: THEME_ICON_SIZE, tip: "Edit action preset" do
push_state(
Dialog::NamePromptDialog,
title: "Rename Action Preset",
renaming: action,
list: window.backend.config.presets.actions,
callback_method: method(:update_action_preset)
)
end
button get_image("#{TAC::ROOT_PATH}/media/icons/trashcan.png"), image_width: THEME_ICON_SIZE, tip: "Delete action preset", **THEME_DANGER_BUTTON do
push_state(
Dialog::ConfirmDialog,
title: "Are you sure?",
message: "Delete action preset and all of its actions and variables?",
callback_method: proc { delete_action_preset(action) }
)
end
end
end
end
end
def update_group_preset(group, name)
group.name = name
window.backend.config.presets.groups.sort_by! { |g| g.name.downcase }
window.backend.config_changed!
populate_group_presets
end
def delete_group_preset(group)
window.backend.config.presets.groups.delete(group)
window.backend.config.presets.groups.sort_by! { |g| g.name.downcase }
window.backend.config_changed!
populate_group_presets
end
def update_action_preset(action, name)
action.name = name
window.backend.config.presets.actions.sort_by! { |a| a.name.downcase }
window.backend.config_changed!
populate_action_presets
end
def delete_action_preset(action)
window.backend.config.presets.actions.delete(action)
window.backend.config.presets.actions.sort_by! { |a| a.name.downcase }
window.backend.config_changed!
populate_action_presets
end
end
end
end

340
lib/pages/search.rb Normal file
View File

@@ -0,0 +1,340 @@
module TAC
class Pages
class Search < Page
def setup
header_bar("Search")
menu_bar.clear do
search = edit_line "", width: 0.9, height: 1.0
button get_image("#{TAC::ROOT_PATH}/media/icons/zoom.png"), image_height: 1.0 do
unless search.value.strip.empty?
search_results = search_config(search.value.downcase.strip)
status_bar.clear do
if search_results.results.size.zero?
subtitle "No results for: \"#{search.value.strip}\""
else
subtitle "Search results for: \"#{search.value.strip}\""
end
end
body.clear do
shared_index = 0
flow(width: 1.0, height: 1.0) do
stack(width: 0.495, height: 1.0, scroll: true) do
if search_results.groups.size.positive?
title "Groups"
search_results.groups.each do |result|
stack(width: 1.0, **THEME_ITEM_CONTAINER_PADDING) do
background shared_index.even? ? THEME_EVEN_COLOR : THEME_ODD_COLOR
button result.highlight(result.group.name), width: 1.0 do
page(TAC::Pages::Editor, { group: result.group, is_search: true })
end
end
shared_index += 1
end
end
if search_results.actions.size.positive?
title "Actions"
search_results.actions.each do |result|
stack(width: 1.0, **THEME_ITEM_CONTAINER_PADDING) do
background shared_index.even? ? THEME_EVEN_COLOR : THEME_ODD_COLOR
button result.highlight(result.action.name), width: 1.0 do
page(TAC::Pages::Editor, { group: result.group, action: result.action, is_search: true })
end
if result.from_comment?
para result.highlight(result.action.comment), width: 1.0
end
end
shared_index += 1
end
end
if search_results.variables.size.positive?
title "Variables"
search_results.variables.each do |result|
stack(width: 1.0, **THEME_ITEM_CONTAINER_PADDING) do
background shared_index.even? ? THEME_EVEN_COLOR : THEME_ODD_COLOR
button "#{result.highlight(result.variable.name)} [#{result.highlight(result.variable.value)}]", width: 1.0 do
page(TAC::Pages::Editor, { group: result.group, action: result.action, variable: result.variable, is_search: true })
end
end
shared_index += 1
end
end
end
stack(width: 0.495, height: 1.0, scroll: true) do
if search_results.group_presets.size.positive?
title "Group Presets"
search_results.group_presets.each do |result|
stack(width: 1.0, **THEME_ITEM_CONTAINER_PADDING) do
background shared_index.even? ? THEME_EVEN_COLOR : THEME_ODD_COLOR
button result.highlight(result.group.name), width: 1.0 do
page(TAC::Pages::Editor, { group: result.group, group_is_preset: true, is_search: true })
end
end
shared_index += 1
end
end
if search_results.action_presets.size.positive?
title "Action Presets"
search_results.action_presets.each do |result|
stack(width: 1.0, **THEME_ITEM_CONTAINER_PADDING) do
background shared_index.even? ? THEME_EVEN_COLOR : THEME_ODD_COLOR
button result.highlight(result.action.name), width: 1.0 do
if result.group.nil?
page(TAC::Pages::Editor, { action: result.action, action_is_preset: true, is_search: true })
else
page(TAC::Pages::Editor, { group: result.group, action: result.action, group_is_preset: true, is_search: true })
end
end
if result.from_comment?
para result.highlight(result.action.comment), width: 1.0
end
end
shared_index += 1
end
end
if search_results.variables_from_presets.size.positive?
title "Variables from Presets"
search_results.variables_from_presets.each do |result|
stack(width: 1.0, **THEME_ITEM_CONTAINER_PADDING) do
background shared_index.even? ? THEME_EVEN_COLOR : THEME_ODD_COLOR
button "#{result.highlight(result.variable.name)} [#{result.highlight(result.variable.value)}]", width: 1.0 do
if result.group.nil?
page(TAC::Pages::Editor, { action: result.action, variable: result.variable, action_is_preset: true, is_search: true })
else
page(TAC::Pages::Editor, { group: result.group, action: result.action, variable: result.variable, group_is_preset: true, is_search: true })
end
end
end
shared_index += 1
end
end
end
end
end
end
end
end
end
def search_config(query)
search_results = SearchResults.new
search_groups(query, search_results)
search_actions(query, search_results)
search_variables(query, search_results)
search_presets(query, search_results)
return search_results
end
def search_groups(query, search_results)
window.backend.config.groups.each do |group|
if group.name.downcase.include?(query)
result = SearchResult.new(group: group, query: query, is_group: true, is_from_name: true)
search_results.results << result
end
end
end
def search_actions(query, search_results)
window.backend.config.groups.each do |group|
group.actions.each do |action|
if action.name.downcase.include?(query)
result = SearchResult.new(group: group, action: action, query: query, is_action: true, is_from_name: true)
search_results.results << result
end
if action.comment.downcase.include?(query)
result = SearchResult.new(group: group, action: action, query: query, is_action: true, is_from_comment: true)
search_results.results << result
end
end
end
end
def search_variables(query, search_results)
window.backend.config.groups.each do |group|
group.actions.each do |action|
action.variables.each do |variable|
if variable.name.downcase.include?(query)
result = SearchResult.new(group: group, action: action, variable: variable, is_variable: true, query: query, is_from_name: true)
search_results.results << result
end
if variable.value.downcase.include?(query)
result = SearchResult.new(group: group, action: action, variable: variable, is_variable: true, query: query, is_from_value: true)
search_results.results << result
end
end
end
end
end
def search_presets(query, search_results)
window.backend.config.presets.groups.each do |group|
if group.name.downcase.include?(query)
result = SearchResult.new(group: group, query: query, is_group: true, is_from_name: true, is_preset: true)
search_results.results << result
end
group.actions.each do |action|
if action.name.downcase.include?(query)
result = SearchResult.new(group: group, action: action, query: query, is_action: true, is_from_name: true, is_preset: true)
search_results.results << result
end
if action.comment.downcase.include?(query)
result = SearchResult.new(group: group, action: action, query: query, is_action: true, is_from_comment: true, is_preset: true)
search_results.results << result
end
action.variables.each do |variable|
if variable.name.downcase.include?(query)
result = SearchResult.new(group: group, action: action, variable: variable, is_variable: true, query: query, is_from_name: true, is_preset: true)
search_results.results << result
end
if variable.value.downcase.include?(query)
result = SearchResult.new(group: group, action: action, variable: variable, is_variable: true, query: query, is_from_value: true, is_preset: true)
search_results.results << result
end
end
end
end
window.backend.config.presets.actions.each do |action|
if action.name.downcase.include?(query)
result = SearchResult.new(group: nil, action: action, query: query, is_action: true, is_from_name: true, is_preset: true)
search_results.results << result
end
if action.comment.downcase.include?(query)
result = SearchResult.new(group: nil, action: action, query: query, is_action: true, is_from_comment: true, is_preset: true)
search_results.results << result
end
action.variables.each do |variable|
if variable.name.downcase.include?(query)
result = SearchResult.new(group: nil, action: action, variable: variable, is_variable: true, query: query, is_from_name: true, is_preset: true)
search_results.results << result
end
if variable.value.downcase.include?(query)
result = SearchResult.new(group: nil, action: action, variable: variable, is_variable: true, query: query, is_from_value: true, is_preset: true)
search_results.results << result
end
end
end
end
class SearchResults
attr_reader :results
def initialize
@results = []
end
def groups
@results.select { |result| result.group? && !result.preset? }
end
def actions
@results.select { |result| result.action? && !result.preset? }
end
def variables
@results.select { |result| result.variable? && !result.preset? }
end
def group_presets
@results.select { |result| result.group? && result.preset? }
end
def action_presets
@results.select { |result| result.action? && result.preset? }
end
def variables_from_presets
@results.select { |result| result.variable? && result.preset? }
end
end
class SearchResult
attr_reader :group, :action, :variable, :query
def initialize(query:, group:, action: nil, variable: nil,
is_group: false, is_action: false, is_variable: false,
is_from_name: false, is_from_value: false, is_from_comment: false, is_preset: false)
@group = group
@action = action
@variable = variable
@query = query
@is_group = is_group
@is_action = is_action
@is_variable = is_variable
@is_from_name = is_from_name
@is_from_value = is_from_value
@is_from_comment = is_from_comment
@is_preset = is_preset
end
def group?
@is_group
end
def action?
@is_action
end
def variable?
@is_variable
end
def from_name?
@is_from_name
end
def from_value?
@is_from_value
end
def from_comment?
@is_from_comment
end
def preset?
@is_preset
end
def highlight(string)
string.gsub(/#{@query}/i, "<b><c=ff00ff>#{@query}</c></b>")
end
end
end
end
end

85
lib/pages/simulator.rb Normal file
View File

@@ -0,0 +1,85 @@
module TAC
class Pages
class Simulator < Page
SOURCE_FILE_PATH = "#{TAC::ROOT_PATH}/data/simulator.rb"
def setup
header_bar("Simulator")
menu_bar.clear do
button get_image("#{TAC::ROOT_PATH}/media/icons/right.png"), tip: "Run Simulation", image_height: 1.0 do
save_source
begin
@simulation = TAC::Simulator::Simulation.new(source_code: @source_code.value, field_container: @field_container)
@simulation.start
rescue SyntaxError, NameError, NoMethodError, TypeError, ArgumentError => e
puts e.backtrace.reverse.join("\n")
puts e
push_state(Dialog::AlertDialog, title: "#{e.class}", message: e)
end
end
button get_image("#{TAC::ROOT_PATH}/media/icons/stop.png"), tip: "Stop Simulation", image_height: 1.0 do
@simulation.robots.each { |robot| robot.queue.clear } if @simulation
end
button get_image("#{TAC::ROOT_PATH}/media/icons/save.png"), tip: "Save", image_height: 1.0 do
save_source
end
end
status_bar.clear do
@simulation_status = label ""
end
body.clear do
flow(width: 1.0, height: 1.0) do
@field_container = stack width: 0.4, height: 1.0 do
background 0xff_111111
end
stack(width: 0.6, height: 1.0) do
source_code =
"robot = create_robot(alliance: :blue, width: 18, depth: 18)
robot.backward 100
robot.turn 90
robot.forward 100
robot.turn -90
robot.forward 100
robot.turn -90
robot.forward 100"
source_code = File.read(SOURCE_FILE_PATH) if File.exists?(SOURCE_FILE_PATH)
@source_code = edit_box source_code, width: 1.0, height: 1.0
end
end
end
end
def save_source
File.open(SOURCE_FILE_PATH, "w") { |f| f.write @source_code.value }
@simulation_status.value = "Saved source to #{SOURCE_FILE_PATH}"
end
def blur
@simulation.robots.each { |robot| robot.queue.clear } if @simulation
save_source
end
def draw
@simulation.draw if @simulation
end
def update
return unless @simulation
@simulation.update
unless @simulation.robots.all? { |robot| robot.queue.empty? } # Only update clock if simulation is running
@simulation_status.value = "Time: #{(@simulation.simulation_time).round(1)} seconds"
end
end
end
end
end

76
lib/pages/tacnet.rb Normal file
View File

@@ -0,0 +1,76 @@
module TAC
class Pages
class TACNET < Page
def setup
header_bar("TimeCrafters Auxiliary Configuration Network")
menu_bar.clear do
@connect_menu = flow(width: 1.0, height: 1.0) do
label "Hostname", text_size: 28
hostname = edit_line window.backend.settings.hostname, width: 0.33, height: 1.0, text_size: 28
label "Port", text_size: 28
port = edit_line window.backend.settings.port, width: 0.33, height: 1.0, text_size: 28
button "Connect", height: 1.0, text_size: 28 do
if hostname.value != window.backend.settings.hostname || port.value.to_i != window.backend.settings.port
window.backend.settings_changed!
end
window.backend.settings.hostname = hostname.value
window.backend.settings.port = port.value.to_i
window.backend.tacnet.connect(hostname.value, port.value.to_i)
end
end
@disconnect_menu = flow(width: 1.0, height: 1.0) do
button "Disconnect", height: 1.0, text_size: 28 do
window.backend.tacnet.close
end
end
end
status_bar.clear do
@tacnet_icon = image "#{TAC::ROOT_PATH}/media/icons/signal3.png", height: 26
@status_label = label "TACNET: Not Connected", text_size: 26
end
body.clear do
@full_status_label = label ""
end
end
def update
case window.backend.tacnet.status
when :connected
sent = "#{window.backend.tacnet.client.packets_sent}".rjust(4, '0')
received = "#{window.backend.tacnet.client.packets_received}".rjust(4, '0')
@status_label.value = "TACNET: Connected Pkt Sent: #{sent} Pkt Received: #{received}"
@full_status_label.value = window.backend.tacnet.full_status
@tacnet_icon.style.color = TAC::Palette::TACNET_CONNECTED
@connect_menu.hide
@disconnect_menu.show
when :connecting
@status_label.value = "TACNET: Connecting..."
@tacnet_icon.style.color = TAC::Palette::TACNET_CONNECTING
@connect_menu.hide
@disconnect_menu.show
when :connection_error
@status_label.value = "TACNET: Connection Error"
@full_status_label.value = window.backend.tacnet.full_status
@tacnet_icon.style.color = TAC::Palette::TACNET_CONNECTION_ERROR
@connect_menu.show
@disconnect_menu.hide
when :not_connected
@status_label.value = "TACNET: Not Connected"
@full_status_label.value = ""
@tacnet_icon.style.color = 0xff_ffffff
@connect_menu.show
@disconnect_menu.hide
end
end
end
end
end

View File

@@ -12,8 +12,8 @@ module TAC
BLUE_ALLIANCE = Gosu::Color.new(0xff_000080)
RED_ALLIANCE = Gosu::Color.new(0xff_800000)
TACNET_PRIMARY = Gosu::Color.new(0xff_003f7f)
TACNET_SECONDARY = Gosu::Color.new(0xff_007f7f)
TACNET_PRIMARY = Gosu::Color.new(0xff000080)
TACNET_SECONDARY = Gosu::Color.new(0xff000060)
GROUPS_PRIMARY = Gosu::Color.new(0xff_444444)
GROUPS_SECONDARY = Gosu::Color.new(0xff_444444)
@@ -26,5 +26,8 @@ module TAC
EDITOR_PRIMARY = Gosu::Color.new(0xff_446688)
EDITOR_SECONDARY = Gosu::Color.new(0xff_224466)
ALERT = TACNET_CONNECTING
DANGEROUS = TACNET_CONNECTION_ERROR
end
end

View File

@@ -1,6 +1,8 @@
module TAC
class Simulator
class Field
attr_reader :scale
def initialize(container:, season:, simulation:)
@container = container
@season = season
@@ -9,21 +11,23 @@ module TAC
@position = CyberarmEngine::Vector.new
@scale = 1
@size = 0
@field_size = 144 # inches [1 pxel = 1 inch]
@field_size = 144 # inches [1 pixel = 1 inch]
@blue = Gosu::Color.new(0xff_004080)
@red = Gosu::Color.new(0xff_800000)
end
def draw
Gosu.flush
Gosu.clip_to(@position.x, @position.y, @size, @size) do
Gosu.translate(@position.x, @position.y) do
draw_field
Gosu.scale(@scale) do
self.send(:"draw_field_#{@season}")
@simulation.robots.each(&:draw)
@simulation.robots.each { |robot| robot.queue.first.draw if robot.queue.first && @simulation.show_paths }
@simulation&.robots&.each(&:draw)
@simulation&.robots&.each { |robot| robot.queue.first.draw if robot.queue.first && @simulation.show_paths }
end
end
end
@@ -94,9 +98,185 @@ module TAC
end
end
def draw_field_ultimate_goal
# middle line
Gosu.draw_rect(0, @field_size / 2 - 13, @field_size, 2, Gosu::Color::WHITE)
# phantom center line to indict half field for remote season field
Gosu.draw_rect(@field_size / 2 - (0.5 + 24), 0, 1, @field_size, 0x88_448844)
# blue starting lines
Gosu.draw_rect(24 - 1, @field_size - 24, 2, 24, @blue)
Gosu.draw_rect(48 - 1, @field_size - 24, 2, 24, @blue)
# blue wobbly wobs
Gosu.draw_circle(24, @field_size - 24, 4, 32, @blue)
Gosu.draw_circle(48, @field_size - 24, 4, 32, @blue)
# blue starter stack
Gosu.draw_rect(36 - 1, @field_size - 50, 2, 2, @blue)
# blue target zones
# A
draw_tile_box(@blue)
# B
Gosu.translate(24, 24) do
draw_tile_box(@blue)
end
# C
Gosu.translate(0, 48) do
draw_tile_box(@blue)
end
# red starting lines
Gosu.draw_rect(@field_size - 24 - 1, @field_size - 24, 2, 24, @red)
Gosu.draw_rect(@field_size - 48 - 1, @field_size - 24, 2, 24, @red)
# red wobbly wobs
Gosu.draw_circle(@field_size - 24, @field_size - 24, 4, 32, @red)
Gosu.draw_circle(@field_size - 48, @field_size - 24, 4, 32, @red)
# red starter stack
Gosu.draw_rect(@field_size - 37, @field_size - 50, 2, 2, @red)
# red target zones
# A
Gosu.translate(@field_size - 24, 0) do
draw_tile_box(@red)
end
# B
Gosu.translate(@field_size - 48, 24) do
draw_tile_box(@red)
end
# C
Gosu.translate(@field_size - 24, 48) do
draw_tile_box(@red)
end
end
def draw_field_freight_frenzy
# blue ZONE
Gosu.draw_rect(24, @field_size - 24, 2, 24, @blue)
Gosu.draw_rect(24, @field_size - 24, 24, 2, @blue)
Gosu.draw_rect(48 - 2, @field_size - 24, 2, 24, @blue)
# blue barcode 1
Gosu.draw_rect(36 - 1, @field_size - 24 - 4, 2, 2, @blue)
Gosu.draw_rect(36 - 1, @field_size - 36 - 1, 2, 2, @blue)
Gosu.draw_rect(36 - 1, @field_size - 48 + 2, 2, 2, @blue)
# blue barcode 2
Gosu.draw_rect(36 - 1, 48 + 2, 2, 2, @blue)
Gosu.draw_rect(36 - 1, 60 - 1, 2, 2, @blue)
Gosu.draw_rect(36 - 1, 72 - 4, 2, 2, @blue)
# blue wobble goal
Gosu.draw_circle(48, 84, 9, 32, @blue)
# blue shared wobble goal
Gosu.draw_circle(@field_size / 2, 24, 9, 32, @blue)
# red ZONE
Gosu.draw_rect(@field_size - 24 - 2, @field_size - 24, 2, 24, @red)
Gosu.draw_rect(@field_size - 48, @field_size - 24, 24, 2, @red)
Gosu.draw_rect(@field_size - 48, @field_size - 24, 2, 24, @red)
# red barcode 1
Gosu.draw_rect(@field_size - 36 - 1, @field_size - 24 - 4, 2, 2, @red)
Gosu.draw_rect(@field_size - 36 - 1, @field_size - 36 - 1, 2, 2, @red)
Gosu.draw_rect(@field_size - 36 - 1, @field_size - 48 + 2, 2, 2, @red)
# red barcode 2
Gosu.draw_rect(@field_size - 36 - 1, 48 + 2, 2, 2, @red)
Gosu.draw_rect(@field_size - 36 - 1, 60 - 1, 2, 2, @red)
Gosu.draw_rect(@field_size - 36 - 1, 72 - 4, 2, 2, @red)
# red wobble goal
Gosu.draw_circle(@field_size - 48, 84, 9, 32, @red)
# red shared wobble goal
Gosu.clip_to(@field_size / 2, 0, 10, 48) do
Gosu.draw_circle(@field_size / 2, 24, 9, 32, @red)
end
# white corner left
faint_white = Gosu::Color.rgb(240, 240, 240)
Gosu.draw_rect(0, 46 - 2, 46, 2, faint_white)
Gosu.draw_rect(46 - 2, 0, 2, 46, faint_white)
# white corner right
Gosu.draw_rect(@field_size - 46, 46 - 2, 46, 2, faint_white)
Gosu.draw_rect(@field_size - 46, 0, 2, 46, faint_white)
# cross bars
bar_gray = Gosu::Color.rgb(50, 50, 50)
# MAIN
Gosu.draw_rect(13.75, 48 - 2, @field_size - 13.75 * 2, 1, bar_gray)
Gosu.draw_rect(13.75, 48 + 1, @field_size - 13.75 * 2, 1, bar_gray)
Gosu.draw_rect(13.75, 48 - 2, 1, 4, Gosu::Color::BLACK)
Gosu.draw_rect(@field_size - 13.75 - 1, 48 - 2, 1, 4, Gosu::Color::BLACK)
# BLUE
Gosu.draw_rect(48 - 2, 13.75, 1, 48 - 13.75 - 2, bar_gray)
Gosu.draw_rect(48 + 1, 13.75, 1, 48 - 13.75 - 2, bar_gray)
Gosu.draw_rect(48 - 2, 13.75, 4, 1, Gosu::Color::BLACK)
Gosu.draw_rect(48 - 2, 48 - 3, 4, 1, Gosu::Color::BLACK)
# RED
Gosu.draw_rect(@field_size - 48 - 2, 13.75, 1, 48 - 13.75 - 2, bar_gray)
Gosu.draw_rect(@field_size - 48 + 1, 13.75, 1, 48 - 13.75 - 2, bar_gray)
Gosu.draw_rect(@field_size - 48 - 2, 13.75, 4, 1, Gosu::Color::BLACK)
Gosu.draw_rect(@field_size - 48 - 2, 48 - 3, 4, 1, Gosu::Color::BLACK)
# Duck Delivery
Gosu.draw_circle(2, @field_size - 2, 9, 16, Gosu::Color.rgb(75, 75, 75))
Gosu.draw_circle(@field_size - 2, @field_size - 2, 9, 16, Gosu::Color.rgb(75, 75, 75))
# packages
soft_orange = Gosu::Color.rgb(255, 175, 0)
7.times do |y|
7.times do |x|
if x.even?
Gosu.draw_rect(x * 3 + 1, y * 3 + 1, 2, 2, soft_orange)
else
Gosu.draw_circle(x * 3 + 2, y * 3 + 2, 1, 16, faint_white)
end
end
end
7.times do |y|
7.times do |x|
if x.even?
Gosu.draw_rect((@field_size - 4) - x * 3 + 1, y * 3 + 1, 2, 2, soft_orange)
else
Gosu.draw_circle((@field_size - 4) - x * 3 + 2, y * 3 + 2, 1, 16, faint_white)
end
end
end
Gosu.draw_rect(0, 60 - 1, 2, 2, soft_orange)
Gosu.draw_rect(0, 108 - 1, 2, 2, soft_orange)
Gosu.draw_rect(@field_size - 2, 60 - 1, 2, 2, soft_orange)
Gosu.draw_rect(@field_size - 2, 108 - 1, 2, 2, soft_orange)
end
def draw_tile_box(color)
Gosu.draw_rect(0, 0, 24, 2, color)
Gosu.draw_rect(22, 2, 2, 22, color)
Gosu.draw_rect(0, 22, 22, 2, color)
Gosu.draw_rect(0, 2, 2, 22, color)
end
def update
@position.x, @position.y = @container.x, @container.y
@size = @container.width
@position.x = @container.x
@position.y = @container.y
@size = [@container.width, @container.height].min
@scale = @size.to_f / @field_size
end
end

View File

@@ -75,6 +75,14 @@ module TAC
@queue << Move.new(robot: self, distance: -distance, power: power)
end
def strafe_right(distance, power = 1.0)
@queue << Strafe.new(robot: self, distance: distance, power: power)
end
def strafe_left(distance, power = 1.0)
@queue << Strafe.new(robot: self, distance: -distance, power: power)
end
def turn(relative_angle, power = 1.0)
@queue << Turn.new(robot: self, relative_angle: relative_angle, power: power)
end
@@ -143,6 +151,52 @@ class State
end
end
class Strafe < State
def initialize(robot:, distance:, power:)
@robot = robot
@distance = distance
@power = power.clamp(-1.0, 1.0)
end
def start
@starting_position = @robot.position.clone
@goal = @starting_position.clone
if @distance.positive?
@goal.x += Math.cos((@robot.angle + 90).gosu_to_radians) * @distance
@goal.y += Math.sin((@robot.angle + 90).gosu_to_radians) * @distance
else
@goal.x += Math.cos((@robot.angle - 90).gosu_to_radians) * @distance
@goal.y += Math.sin((@robot.angle - 90).gosu_to_radians) * @distance
end
@complete = false
@allowable_error = 1.0
end
def draw
Gosu.draw_line(
@robot.position.x + @robot.width / 2, @robot.position.y + @robot.depth / 2, TAC::Palette::TIMECRAFTERS_TERTIARY,
@goal.x + @robot.width / 2, @goal.y + @robot.depth / 2, TAC::Palette::TIMECRAFTERS_TERTIARY
)
Gosu.draw_rect(@goal.x + (@robot.width / 2 - 1), @goal.y + (@robot.depth / 2 - 1), 2, 2, Gosu::Color::RED)
end
def update(dt)
speed = (@distance > 0 ? @power * dt : -@power * dt) * @robot.speed
if @robot.position.distance(@goal) <= @allowable_error
@complete = true
@robot.position = @goal
else
if speed > 0
@robot.position -= (@robot.position - @goal).normalized * speed
else
@robot.position += (@robot.position - @goal).normalized * speed
end
end
end
end
class Turn < State
def initialize(robot:, relative_angle:, power:)
@robot = robot

View File

@@ -1,16 +1,20 @@
module TAC
class Simulator
class Simulation
attr_reader :robots, :show_paths
attr_reader :robots, :show_paths, :simulation_time
def initialize(source_code:, field_container:)
@source_code = source_code
@field_container = field_container
@robots = []
@field = Field.new(simulation: self, season: :skystone, container: @field_container)
@field = Field.new(simulation: self, season: :freight_frenzy, container: @field_container)
@show_paths = false
@last_milliseconds = Gosu.milliseconds
@simulation_step = 1.0 / 60.0
@accumulator = 0.0
@simulation_time = 0.0
end
def start
@@ -23,8 +27,15 @@ module TAC
end
def update
@field.update
@robots.each { |robot| robot.update((Gosu.milliseconds - @last_milliseconds) / 1000.0) }
@accumulator += (Gosu.milliseconds - @last_milliseconds) / 1000.0
while(@accumulator > @simulation_step)
@field.update
@robots.each { |robot| robot.update(@simulation_step) }
@accumulator -= @simulation_step
@simulation_time += @simulation_step
end
@last_milliseconds = Gosu.milliseconds
end

View File

@@ -2,22 +2,28 @@ module TAC
class States
class Boot < CyberarmEngine::GuiState
def setup
window.show_cursor = true
stack width: 1.0, height: 1.0 do
background [TAC::Palette::TIMECRAFTERS_PRIMARY, TAC::Palette::TIMECRAFTERS_SECONDARY, TAC::Palette::TIMECRAFTERS_TERTIARY, TAC::Palette::TIMECRAFTERS_PRIMARY]
end
@title_font = CyberarmEngine::Text.new(TAC::NAME, z: 100, size: 72, shadow: true, shadow_size: 3, font: THEME[:Label][:font])
@title_font = CyberarmEngine::Text.new(TAC::NAME, z: 100, size: 72, border: true, border_size: 3, font: THEME[:Label][:font])
@logo = Gosu::Image.new("#{TAC::ROOT_PATH}/media/logo.png")
@animator = CyberarmEngine::Animator.new(start_time: 0, duration: 3_000, from: 0, to: 255)
@transition_color = Gosu::Color.new(0x00_000000)
@title_animator = CyberarmEngine::Animator.new(start_time: 0, duration: 750, from: 0.0, to: 1.0, tween: :swing_from_to)
@logo_animator = CyberarmEngine::Animator.new(start_time: 750, duration: 1_000, from: 0.0, to: 1.0, tween: :swing_to)
@transition_animator = CyberarmEngine::Animator.new(start_time: 2_250, duration: 750, from: 0, to: 255, tween: :ease_out)
@transition_color = Gosu::Color.new(0x00_111111)
@next_state = Editor
end
def draw
super
@title_font.draw
@logo.draw(window.width / 2 - @logo.width / 2, window.height / 2 - @logo.height / 2, 99)
@logo.draw_rot(window.width / 2, window.height / 2, 99, 0, 0.5, 0.5, @logo_animator.transition, @logo_animator.transition)
Gosu.draw_rect(0, 0, window.width, window.height, @transition_color, 10_00)
end
@@ -25,17 +31,17 @@ module TAC
super
@title_font.x = window.width / 2 - @title_font.width / 2
@title_font.y = window.height / 2 - (@logo.height / 2 + @title_font.height)
@title_font.y = (window.height / 2 - (@logo.height / 2 + @title_font.height)) * @title_animator.transition
@transition_color.alpha = @animator.transition(0, 255, :sine)
@transition_color.alpha = @transition_animator.transition
push_state(Editor) if @transition_color.alpha >= 255
push_state(@next_state) if @transition_color.alpha >= 255
end
def button_up(id)
super
push_state(Editor)
push_state(@next_state)
end
end
end

View File

@@ -1,379 +1,214 @@
module TAC
class States
class Editor < CyberarmEngine::GuiState
def setup
@active_group = nil
@active_action = nil
class Editor < CyberarmEngine::GuiState
include CyberarmEngine::Theme # get access to deep_merge method
attr_reader :header_bar, :header_bar_label, :navigation, :content, :menu_bar, :status_bar, :body
theme(THEME)
def setup
window.show_cursor = true
stack width: 1.0, height: 1.0 do
stack width: 1.0, height: 0.1, border_thickness: 1, border_color: [0, 0, Gosu::Color::BLACK, 0] do
background THEME_HEADER_BACKGROUND
@window_width = 0
@window_height = 0
flow width: 1.0, height: 1.0 do
stack width: 0.60 do
label TAC::NAME, bold: true, text_size: THEME_HEADING_TEXT_SIZE
flow width: 1.0 do
flow width: 0.3 do
label "Group: "
@active_group_label = label ""
end
@pages = {}
@page = nil
flow width: 0.3 do
label "Action: "
@active_action_label = label ""
end
# TODO: Use these colors for buttons
_theme = {
Button: {
background: 0xff_006000,
border_color: 0x88_111111,
hover: {
color: 0xff_ffffff,
background: 0xff_00d000,
border_color: 0x88_111111
},
active: {
color: 0xff_ffffff,
background: 0xff_004000,
border_color: 0x88_111111
}
}
}
flow width: 0.395 do
button get_image("#{TAC::ROOT_PATH}/media/icons/right.png"), image_width: THEME_ICON_SIZE, margin_left: 10, tip: "Simulator" do
push_state(Simulator)
end
button get_image("#{TAC::ROOT_PATH}/media/icons/menuList.png"), image_width: THEME_ICON_SIZE, margin_left: 10, tip: "Manage presets" do
push_state(ManagePresets)
end
button get_image("#{TAC::ROOT_PATH}/media/icons/wrench.png"), image_width: THEME_ICON_SIZE, margin_left: 10, tip: "Manage configurations" do
push_state(ManageConfigurations)
end
button get_image("#{TAC::ROOT_PATH}/media/icons/save.png"), image_width: THEME_ICON_SIZE, margin_left: 10, tip: "Save config and settings to disk" do
window.backend.save_config(window.backend.settings.config)
window.backend.save_settings
end
button get_image("#{TAC::ROOT_PATH}/media/icons/export.png"), image_width: THEME_ICON_SIZE, margin_left: 10, tip: "Upload local config to remote, if connected." do
window.backend.upload_config(window.backend.settings.config)
end
button get_image("#{TAC::ROOT_PATH}/media/icons/import.png"), image_width: THEME_ICON_SIZE, margin_left: 10, tip: "Download remote config, if connected." do
window.backend.download_config(window.backend.settings.config)
end
end
end
end
theme(deep_merge(TAC::THEME, _theme))
flow width: 0.399 do
stack width: 0.5 do
label "TACNET v#{TACNET::Packet::PROTOCOL_VERSION}", color: TAC::Palette::TACNET_PRIMARY, text_shadow: true, text_shadow_size: 1, text_shadow_color: Gosu::Color::BLACK
flow width: 1.0 do
@tacnet_hostname = edit_line "#{window.backend.settings.hostname}", width: 0.5, margin_right: 0
@tacnet_hostname.subscribe(:changed) do |caller, value|
window.backend.settings.hostname = value
window.backend.settings_changed!
end
@header_bar = flow(width: 1.0, height: 36) do
background 0xff_006000
label ":", margin: 0, padding: 0, padding_top: 3
@header_bar_label = label TAC::NAME, width: 1.0, text_align: :center, text_size: 32
@tacnet_port = edit_line "#{window.backend.settings.port}", width: 0.2, margin_left: 0
@tacnet_port.subscribe(:changed) do |caller, value|
window.backend.settings.port = Integer(value)
window.backend.settings_changed!
end
end
end
stack width: 0.499 do
@tacnet_status = label "Not Connected", background: TAC::Palette::TACNET_NOT_CONNECTED, width: 1.0, padding: 5, margin_top: 2, border_thickness: 1, border_color: Gosu::Color::GRAY
flow width: 1.0 do
@tacnet_connection_button = button "Connect", width: 0.475 do
case window.backend.tacnet.status
when :connected, :connecting
window.backend.tacnet.close
when :not_connected, :connection_error
window.backend.tacnet.connect(@tacnet_hostname.value, @tacnet_port.value)
end
end
button get_image("#{TAC::ROOT_PATH}/media/icons/information.png"), image_width: THEME_ICON_SIZE, width: 0.475 do
push_state(Dialog::AlertDialog, title: "TACNET Status", message: window.backend.tacnet.full_status)
end
end
end
end
end
end
@content = flow width: 1.0, height: 0.9 do
background THEME_CONTENT_BACKGROUND
stack width: 0.333, height: 1.0, border_thickness: 1, border_color: [0, Gosu::Color::BLACK, 0, 0] do
flow do
label "Groups", text_size: THEME_SUBHEADING_TEXT_SIZE
button get_image("#{TAC::ROOT_PATH}/media/icons/plus.png"), image_width: THEME_ICON_SIZE, tip: "Add group" do
push_state(TAC::Dialog::NamePromptDialog, title: "Create Group", list: window.backend.config.groups, callback_method: method(:create_group))
end
button get_image("#{TAC::ROOT_PATH}/media/icons/button2.png"), image_width: THEME_ICON_SIZE, tip: "Clone currently selected group" do
if @active_group
push_state(Dialog::NamePromptDialog, title: "Clone Group", renaming: @active_group, accept_label: "Clone", list: window.backend.config.groups, callback_method: proc { |group, name|
clone = TAC::Config::Group.from_json( JSON.parse( @active_group.to_json, symbolize_names: true ))
clone.name = "#{name}"
window.backend.config.groups << clone
window.backend.config_changed!
populate_groups_list
})
end
end
button get_image("#{TAC::ROOT_PATH}/media/icons/save.png"), image_width: THEME_ICON_SIZE, tip: "Save group as preset" do
if @active_group
push_state(Dialog::NamePromptDialog, title: "Save Group Preset", renaming: @active_group, accept_label: "Save", list: window.backend.config.presets.actions, callback_method: proc { |action, name|
})
end
end
end
@groups_list = stack width: 1.0 do
end
end
stack width: 0.333, height: 1.0, border_thickness: 1, border_color: [0, Gosu::Color::BLACK, 0, 0] do
flow do
label "Actions", text_size: THEME_SUBHEADING_TEXT_SIZE
button get_image("#{TAC::ROOT_PATH}/media/icons/plus.png"), image_width: THEME_ICON_SIZE, tip: "Add action" do
if @active_group
push_state(TAC::Dialog::NamePromptDialog, title: "Create Action", list: @active_group.actions, callback_method: method(:create_action))
else
push_state(TAC::Dialog::AlertDialog, title: "Error", message: "Unable to create action,\nno group selected.")
end
end
button get_image("#{TAC::ROOT_PATH}/media/icons/button2.png"), image_width: THEME_ICON_SIZE, tip: "Clone currently selected action" do
if @active_group && @active_action
push_state(Dialog::NamePromptDialog, title: "Clone Action", renaming: @active_action, accept_label: "Clone", list: @active_group.actions, callback_method: proc { |action, name|
clone = TAC::Config::Action.from_json( JSON.parse( @active_action.to_json, symbolize_names: true ))
clone.name = name
@active_group.actions << clone
window.backend.config_changed!
populate_actions_list(@active_group)
})
end
end
button get_image("#{TAC::ROOT_PATH}/media/icons/save.png"), image_width: THEME_ICON_SIZE, tip: "Save action as preset" do
if @active_action
push_state(Dialog::NamePromptDialog, title: "Save Action Preset", renaming: @active_action, accept_label: "Save", list: window.backend.config.presets.actions, callback_method: proc { |action, name|
})
end
end
end
@actions_list = stack width: 1.0 do
end
end
stack width: 0.331, height: 1.0 do
flow do
label "Variables", text_size: THEME_SUBHEADING_TEXT_SIZE
button get_image("#{TAC::ROOT_PATH}/media/icons/plus.png"), image_width: THEME_ICON_SIZE, tip: "Add variable" do
if @active_action
push_state(TAC::Dialog::VariableDialog, title: "Create Variable", callback_method: method(:create_variable))
else
push_state(TAC::Dialog::AlertDialog, title: "Error", message: "Unable to create variable,\nno action selected.")
end
end
end
@variables_list = stack width: 1.0 do
end
end
end
@window_controls = flow(x: window.width - 36 * 2, y: 0, height: 1.0) do
button get_image("#{TAC::ROOT_PATH}/media/icons/minus.png"), tip: "Minimize", image_height: 1.0 do
window.minimize if window.respond_to?(:minimize)
end
if window.backend.settings.config == nil || window.backend.config == nil
push_state(ManageConfigurations)
else
populate_groups_list
button get_image("#{TAC::ROOT_PATH}/media/icons/larger.png"), tip: "Maximize", image_height: 1.0 do |btn|
window.maximize if window.respond_to?(:maximize)
end
@tacnet_status_monitor = CyberarmEngine::Timer.new(250) do
case window.backend.tacnet.status
when :connected
@tacnet_status.value = "Connected"
@tacnet_status.background = TAC::Palette::TACNET_CONNECTED
@tacnet_connection_button.value = "Disconnect"
when :connecting
@tacnet_status.value = "Connecting..."
@tacnet_status.background = TAC::Palette::TACNET_CONNECTING
@tacnet_connection_button.value = "Disconnect"
when :connection_error
@tacnet_status.value = "Connection Error"
@tacnet_status.background = TAC::Palette::TACNET_CONNECTION_ERROR
@tacnet_connection_button.value = "Connect"
when :not_connected
@tacnet_status.value = "Not Connected"
@tacnet_status.background = TAC::Palette::TACNET_NOT_CONNECTED
@tacnet_connection_button.value = "Connect"
end
button get_image("#{TAC::ROOT_PATH}/media/icons/cross.png"), tip: "Exit", image_height: 1.0, **TAC::THEME_DANGER_BUTTON do
window.close
end
end
def update
super
@tacnet_status_monitor.update
end
def create_group(name)
window.backend.config.groups << TAC::Config::Group.new(name: name, actions: [])
window.backend.config_changed!
populate_groups_list
end
def update_group(group, name)
group.name = name
window.backend.config_changed!
populate_groups_list
end
def delete_group(group)
window.backend.config.groups.delete(group)
window.backend.config_changed!
@active_group = nil
@active_group_label.value = ""
@active_action = nil
@active_action_label.value = ""
@actions_list.clear
@variables_list.clear
populate_groups_list
end
def create_action(name)
@active_group.actions << TAC::Config::Action.new(name: name, enabled: true, variables: [])
window.backend.config_changed!
populate_actions_list(@active_group)
end
def update_action(action, name)
action.name = name
window.backend.config_changed!
populate_actions_list(@active_group)
end
def delete_action(action)
@active_group.actions.delete(action)
window.backend.config_changed!
@active_action = nil
@active_action_label.value = ""
@variables_list.clear
populate_actions_list(@active_group)
end
def create_variable(name, type, value)
@active_action.variables << TAC::Config::Variable.new(name: name, type: type, value: value)
window.backend.config_changed!
populate_variables_list(@active_action)
end
def update_variable(variable, name, type, value)
variable.name = name
variable.type = type
variable.value = value
window.backend.config_changed!
populate_variables_list(@active_action)
end
def delete_variable(variable)
@active_action.variables.delete(variable)
window.backend.config_changed!
populate_variables_list(@active_action)
end
def populate_groups_list
groups = window.backend.config.groups
@groups_list.clear do
groups.each_with_index do |group, i|
flow width: 1.0, **THEME_ITEM_CONTAINER_PADDING do
background i.even? ? THEME_EVEN_COLOR : THEME_ODD_COLOR
button group.name, width: 0.855 do
@active_group = group
@active_group_label.value = group.name
@active_action = nil
@active_action_label.value = ""
populate_actions_list(group)
@variables_list.clear
end
button get_image("#{TAC::ROOT_PATH}/media/icons/gear.png"), image_width: THEME_ICON_SIZE, tip: "Edit group" do
push_state(Dialog::NamePromptDialog, title: "Rename Group", renaming: group, list: window.backend.config.groups, callback_method: method(:update_group))
end
button get_image("#{TAC::ROOT_PATH}/media/icons/trashcan.png"), image_width: THEME_ICON_SIZE, tip: "Delete group", **THEME_DANGER_BUTTON do
push_state(Dialog::ConfirmDialog, title: "Are you sure?", message: "Delete group and all\nof its actions and variables?", callback_method: proc { delete_group(group) })
end
end
end
end
end
def populate_actions_list(group)
actions = group.actions
@actions_list.clear do
actions.each_with_index do |action, i|
flow width: 1.0, **THEME_ITEM_CONTAINER_PADDING do
background i.even? ? THEME_EVEN_COLOR : THEME_ODD_COLOR
button action.name, width: 0.8 do
@active_action = action
@active_action_label.value = action.name
populate_variables_list(action)
end
toggle_button tip: "Enable action"
button get_image("#{TAC::ROOT_PATH}/media/icons/gear.png"), image_width: THEME_ICON_SIZE, tip: "Edit action" do
push_state(Dialog::NamePromptDialog, title: "Rename Action", renaming: action, list: @active_group.actions, callback_method: method(:update_action))
end
button get_image("#{TAC::ROOT_PATH}/media/icons/trashcan.png"), image_width: THEME_ICON_SIZE, tip: "Delete action", **THEME_DANGER_BUTTON do
push_state(Dialog::ConfirmDialog, title: "Are you sure?", message: "Delete action and all\nof its variables?", callback_method: proc { delete_action(action) })
end
end
end
end
end
def populate_variables_list(action)
variables = action.variables
@variables_list.clear do
variables.each_with_index do |variable, i|
flow width: 1.0, **THEME_ITEM_CONTAINER_PADDING do
background i.even? ? THEME_EVEN_COLOR : THEME_ODD_COLOR
button "#{variable.name} [Type: #{variable.type}, Value: #{variable.value}]", width: 0.925, tip: "Edit variable" do
push_state(Dialog::VariableDialog, title: "Edit Variable", variable: variable, callback_method: method(:update_variable))
end
button get_image("#{TAC::ROOT_PATH}/media/icons/trashcan.png"), image_width: THEME_ICON_SIZE, tip: "Delete variable", **THEME_DANGER_BUTTON do
push_state(Dialog::ConfirmDialog, title: "Are you sure?", message: "Delete variable?", callback_method: proc { delete_variable(variable) })
end
end
end
end
end
def refresh_config
@active_group = nil
@active_group_label.value = ""
@active_action = nil
@active_action_label.value = ""
@groups_list.clear
@actions_list.clear
@variables_list.clear
populate_groups_list
end
end
@container = flow(width: 1.0, height: 1.0) do
@navigation = stack(width: 64, height: 1.0, scroll: true) do
background 0xff_333333
button get_image("#{TAC::ROOT_PATH}/media/icons/home.png"), margin: 4, tip: "Home", image_width: 1.0 do
page(TAC::Pages::Home)
end
button get_image("#{TAC::ROOT_PATH}/media/icons/menuList.png"), margin: 4, tip: "Editor", image_width: 1.0 do
page(TAC::Pages::Editor)
end
@tacnet_button = button get_image("#{TAC::ROOT_PATH}/media/icons/signal3.png"), margin: 4, tip: "TACNET", image_width: 1.0 do
page(TAC::Pages::TACNET)
end
button get_image("#{TAC::ROOT_PATH}/media/icons/gear.png"), margin: 4, tip: "Configurations", image_width: 1.0 do
page(TAC::Pages::Configurations)
end
button get_image("#{TAC::ROOT_PATH}/media/icons/menuGrid.png"), margin: 4, tip: "Presets", image_width: 1.0 do
page(TAC::Pages::Presets)
end
button get_image("#{TAC::ROOT_PATH}/media/icons/zoom.png"), margin: 4, tip: "Search", image_width: 1.0 do
page(TAC::Pages::Search)
end
stack(margin_left: 4, width: 1.0, margin_right: 4) do
background 0xff_444444
para "Tools", width: 1.0, text_align: :center
end
button get_image("#{TAC::ROOT_PATH}/media/icons/right.png"), margin: 4, tip: "Simulator", image_width: 1.0 do
page(TAC::Pages::Simulator)
end
button get_image("#{TAC::ROOT_PATH}/media/icons/joystickLeft.png"), margin: 4, tip: "Field Planner", image_width: 1.0 do
page(TAC::Pages::FieldPlanner)
end
button get_image("#{TAC::ROOT_PATH}/media/icons/massiveMultiplayer.png"), margin: 4, tip: "Drive Team Rotation Generator", image_width: 1.0 do
page(TAC::Pages::DriveTeamRotationGenerator)
end
button get_image("#{TAC::ROOT_PATH}/media/icons/custom_stopWatch.png"), margin: 4, tip: "Game Clock", image_width: 1.0 do
page(TAC::Pages::GameClock)
end
button get_image("#{TAC::ROOT_PATH}/media/icons/power.png"), margin: 4, tip: "Exit", image_width: 1.0, **TAC::THEME_DANGER_BUTTON do
window.close
end
end
@content = stack(width: window.width - @navigation.style.width, height: 1.0) do
@chrome = stack(width: 1.0, height: 96) do
@menu_bar = flow(width: 1.0, height: 48, padding: 8) do
background 0xff_008000
end
@status_bar = flow(width: 1.0, height: 96 - 48, padding: 2) do
background 0xff_006000
end
end
@body = stack(width: 1.0, height: 1.0) do
background 0xff_707070
end
end
end
@window_controls.hide unless BORDERLESS
page(TAC::Pages::Home)
end
def draw
super
@page.draw if @page
end
def update
super
@page.update if @page
case window.backend.tacnet.status
when :not_connected
@tacnet_button.style.color = Gosu::Color::WHITE
when :connecting
@tacnet_button.style.color = TAC::Palette::TACNET_CONNECTING
when :connected
@tacnet_button.style.color = TAC::Palette::TACNET_CONNECTED
when :connection_error
@tacnet_button.style.color = TAC::Palette::TACNET_CONNECTION_ERROR
end
window.width = Gosu.available_width / 2 if window.width < Gosu.available_width / 2
window.height = Gosu.available_height / 2 if window.height < Gosu.available_height / 2
if window.width != @window_width || window.height != @window_height
@window_width = window.width
@window_height = window.height
recalc
end
end
def button_down(id)
super
@page&.button_down(id)
end
def button_up(id)
super
@page&.button_up(id)
end
def recalc
@window_controls.style.x = window.width - @window_controls.width
@container.style.height = window.height - @header_bar.height
@content.style.width = window.width - @navigation.width
@body.style.height = window.height - (@chrome.height + @header_bar.height)
request_recalculate
end
def page(klass, options = {})
@menu_bar.clear
@status_bar.clear
@body.clear
if window.backend.settings.config.empty? && page_requires_configuration?(klass)
push_state(TAC::Dialog::AlertDialog, title: "No Config Loaded", message: "A config must be loaded.")
page(TAC::Pages::Configurations)
return
end
@page.blur if @page
@pages[klass] = klass.new(host: self) unless @pages[klass]
@page = @pages[klass]
@page.options = options
@page.setup
@page.focus
end
def page_requires_configuration?(klass)
[
TAC::Pages::Editor,
TAC::Pages::Presets,
TAC::Pages::Search
].include?(klass)
end
end

View File

@@ -1,45 +0,0 @@
module TAC
class States
class ManagePresets < CyberarmEngine::GuiState
def setup
theme(THEME)
stack width: 1.0, height: 0.1 do
background THEME_HEADER_BACKGROUND
label "#{TAC::NAME} ― Manage Presets", bold: true, text_size: THEME_HEADING_TEXT_SIZE
button "Close" do
pop_state
end
end
flow width: 1.0, height: 0.9 do
stack width: 0.33, height: 1.0 do
background TAC::Palette::GROUPS_PRIMARY
label "Group Presets"
# TAC::Storage.group_presets.each do |preset|
%w{ Hello World How Are You }.each do |preset|
button preset, width:1.0
end
label "Action Presets"
# TAC::Storage.action_presets.each do |preset|
%w{ Hello World How Are You }.each do |preset|
button preset, width:1.0
end
end
stack width: 0.6698, height: 1.0 do
background TAC::Palette::EDITOR_PRIMARY
label "Editor"
@editor = stack width: 1.0, height: 1.0, margin: 10 do
background TAC::Palette::EDITOR_SECONDARY
label "HELLO WORLD"
end
end
end
end
end
end
end

View File

@@ -1,81 +0,0 @@
module TAC
class States
class Simulator < CyberarmEngine::GuiState
def setup
theme(THEME)
stack width: 1.0, height: 0.1 do
background THEME_HEADER_BACKGROUND
label "#{TAC::NAME} ― Simulator", bold: true, text_size: THEME_HEADING_TEXT_SIZE
button "Close" do
pop_state
end
end
flow width: 1.0, height: 0.9 do
@field_container = stack width: 0.4, height: 1.0 do
background Gosu::Color.new(0xff_333333)..Gosu::Color::BLACK
end
stack width: 0.6, height: 1.0 do
background Gosu::Color.new(0x88_ff8800)
flow width: 1.0, height: 0.05 do
button get_image("#{TAC::ROOT_PATH}/media/icons/right.png"), image_width: THEME_ICON_SIZE, width: 0.49, tip: "Run simulation" do
begin
@simulation_start_time = Gosu.milliseconds
@simulation = TAC::Simulator::Simulation.new(source_code: @source_code.value, field_container: @field_container)
@simulation.start
rescue SyntaxError, NameError, NoMethodError, TypeError, ArgumentError => e
puts e.backtrace.reverse.join("\n")
puts e
push_state(Dialog::AlertDialog, title: "#{e.class}", message: e)
end
end
button get_image("#{TAC::ROOT_PATH}/media/icons/stop.png"), image_width: THEME_ICON_SIZE, width: 0.49, tip: "Stop simulation" do
@simulation.robots.each { |robot| robot.queue.clear } if @simulation
end
button get_image("#{TAC::ROOT_PATH}/media/icons/save.png"), image_width: THEME_ICON_SIZE, width: 0.49, tip: "Save source code" do
File.open("#{TAC::ROOT_PATH}/data/simulator.rb", "w") { |f| f.write @source_code.value }
end
@simulation_status = label ""
end
stack width: 1.0, height: 0.95 do
source_code = ""
if File.exist?("#{TAC::ROOT_PATH}/data/simulator.rb")
source_code = File.read("#{TAC::ROOT_PATH}/data/simulator.rb")
else
source_code =
"robot = create_robot(alliance: :blue, width: 18, depth: 18)
robot.backward 100
robot.turn 90
robot.forward 100
robot.turn -90
robot.forward 100
robot.turn -90
robot.forward 100"
end
@source_code = edit_box source_code, width: 1.0, height: 1.0
end
end
end
end
def draw
super
Gosu.flush
@simulation.draw if @simulation
end
def update
super
@simulation.update if @simulation
@simulation_status.value = "Time: #{((Gosu.milliseconds - @simulation_start_time) / 1000.0).round(1)} seconds" if @simulation_start_time
end
end
end
end

View File

@@ -41,15 +41,15 @@ module TAC
net_stats += "<b>Data Received:</b> #{client.data_received} bytes\n"
"<b>Status:</b> #{_status}\n\n#{net_stats}"
elsif @connection && @connection.client && @connection.client.socket_error?
"<b>Status:</b> #{_status}\n\n#{@connection.client.last_socket_error.to_s.chars.each_slice(32).to_a.map { |c| c.join }.join("\n")}"
elsif @connection&.client && @connection.client.socket_error?
"<b>Status:</b> #{_status}\n\n#{@connection.client.last_socket_error.to_s}"
else
"<b>Status:</b> #{_status}"
end
end
def connected?
@connection && @connection.connected?
@connection&.connected?
end
def close
@@ -60,7 +60,7 @@ module TAC
end
def client
@connection.client if connected?
@connection.client
end
def puts(packet)

View File

@@ -2,14 +2,21 @@ module TAC
class TACNET
class Packet
PROTOCOL_VERSION = 1
PROTOCOL_HEADER_SEPERATOR = "|"
PROTOCOL_SEPERATOR = "|"
PROTOCOL_HEARTBEAT = "heartbeat"
PACKET_TYPES = {
handshake: 0,
heartbeat: 1,
download_config: 2,
upload_config: 3,
error: 2,
download_config: 10,
upload_config: 11,
list_configs: 12,
select_config: 13,
add_config: 14,
update_config: 15,
delete_config: 16,
add_group: 20,
update_group: 21,
@@ -28,7 +35,7 @@ module TAC
slice = message.split("|", 4)
if slice.size < 4
warn "Failed to split packet along first 4 " + PROTOCOL_HEADER_SEPERATOR + ". Raw return: " + slice.to_s
warn "Failed to split packet along first 4 " + PROTOCOL_SEPERATOR + ". Raw return: " + slice.to_s
return nil
end
@@ -71,11 +78,11 @@ module TAC
def encode_header
string = ""
string += protocol_version.to_s
string += PROTOCOL_HEADER_SEPERATOR
string += PROTOCOL_SEPERATOR
string += PACKET_TYPES[type].to_s
string += PROTOCOL_HEADER_SEPERATOR
string += PROTOCOL_SEPERATOR
string += content_length.to_s
string += PROTOCOL_HEADER_SEPERATOR
string += PROTOCOL_SEPERATOR
return string
end

View File

@@ -10,6 +10,7 @@ module TAC
packet = Packet.from_stream(message)
if packet
log.i(TAG, "Received packet of type: #{packet.type}")
hand_off(packet)
else
log.d(TAG, "Rejected raw packet: #{message}")
@@ -22,10 +23,23 @@ module TAC
handle_handshake(packet)
when :heartbeat
handle_heartbeat(packet)
when :error
handle_error(packet)
when :download_config
handle_download_config(packet)
when :upload_config
handle_upload_config(packet)
when :list_configs
handle_list_configs(packet)
when :select_config
handle_select_config(packet)
when :add_config
handle_add_config(packet)
when :update_config # rename config/file
handle_update_config(packet)
when :delete_config
handle_delete_config(packet)
else
log.d(TAG, "No hand off available for packet type: #{packet.type}")
end
@@ -41,52 +55,134 @@ module TAC
def handle_heartbeat(packet)
end
# TODO: Handle errors
def handle_error(packet)
if @host_is_a_connection
title, message = packet.body.split(Packet::PROTOCOL_SEPERATOR, 2)
$window.push_state(TAC::Dialog::TACNETDialog, title: title, message: message)
else
log.e(TAG, "Remote error: #{title}: #{message}")
end
end
def handle_upload_config(packet)
begin
data = JSON.parse(packet.body, symbolize_names: true)
config_name, json = packet.body.split(Packet::PROTOCOL_SEPERATOR, 2)
data = JSON.parse(json, symbolize_names: true)
if @host_is_a_connection
if data.is_a?(Array)
# OLDEST CONFIG, upgrade?
$window.push_state(TAC::Dialog::AlertDialog, title: "Invalid Config", message: "Remote config to old.")
if data.is_a?(Hash) && data.dig(:config, :spec_version) == TAC::CONFIG_SPEC_VERSION
File.open("#{TAC::CONFIGS_PATH}/#{config_name}.json", "w") { |f| f.write json }
elsif data.is_a?(Hash) && data.dig(:config, :spec_version) == TAC::CONFIG_SPEC_VERSION
$window.push_state(TAC::Dialog::ConfirmDialog, title: "Replace Config", message: "Replace local config\nwith remote config?", callback_method: proc {
File.open("#{TAC::ROOT_PATH}/data/config.json", "w") { |f| f.write packet.body }
$window.backend.update_config
})
elsif data.is_a?(Hash) && data.dig(:config, :spec_version) < TAC::CONFIG_SPEC_VERSION
# OLD CONFIG, Upgrade?
$window.push_state(TAC::Dialog::ConfirmDialog, title: "Upgrade Config", message: "Remote config is an older\nspec version.\nTry to upgrade?", callback_method: proc {})
elsif data.is_a?(Hash) && data.dig(:config, :spec_version) > TAC::CONFIG_SPEC_VERSION
# NEWER CONFIG, Error Out
$window.push_state(TAC::Dialog::AlertDialog, title: "Invalid Config", message: "Client outdated, check for\nupdates.\nSupported config spec:\nv#{TAC::CONFIG_SPEC_VERSION} got v#{data.dig(:config, :spec_version)}")
else
# CONFIG is unknown
$window.push_state(TAC::Dialog::AlertDialog, title: "Invalid Config", message: "Remote config is not supported.")
if $window.backend.config&.name == config_name
$window.backend.load_config(config_name)
end
else
$window.push_state(TAC::Dialog::AlertDialog, title: "Invalid Config", message: "Supported config spec: v#{TAC::CONFIG_SPEC_VERSION} got v#{data.dig(:config, :spec_version)}")
end
rescue JSON::ParserError => e
log.e(TAG, "JSON parsing error: #{e}")
end
end
def handle_download_config(packet)
config_name = packet.body
log.i(TAG, config_name)
pkt = nil
if File.exist?("#{TAC::CONFIGS_PATH}/#{config_name}.json")
pkt = PacketHandler.packet_upload_config(config_name, Config.new(config_name).to_json)
else
pkt = PacketHandler.packet_error("Remote config not found", "The requested config #{config_name} does not exist over here.")
end
if @host_is_a_connection
json = JSON.dump($window.backend.config)
$window.backend.tacnet.puts(PacketHandler.packet_upload_config(json))
$window.backend.tacnet.puts(pkt)
else
$server.active_client.puts(pkt)
end
end
def handle_list_configs(packet)
if @host_is_a_connection # Download new or updated configs
list = packet.body.split(Packet::PROTOCOL_SEPERATOR).map { |part| part.split(",") }
remote_configs = list.map { |l| l.first }
local_configs = Dir.glob("#{TAC::CONFIGS_PATH}/*.json").map { |f| File.basename(f, ".json") }
_diff = local_configs - remote_configs
list.each do |name, revision|
revision = Integer(revision)
path = "#{TAC::CONFIGS_PATH}/#{name}.json"
if File.exist?(path)
config = Config.new(name)
if config.configuration.revision < revision
$window.backend.tacnet.puts( PacketHandler.packet_download_config(name) )
elsif config.configuration.revision > revision
$window.backend.tacnet.puts( PacketHandler.packet_upload_config(name, JSON.dump( config )) )
end
else
$window.backend.tacnet.puts( PacketHandler.packet_download_config(name) )
end
end
_diff.each do |name|
config = Config.new(name)
$window.backend.tacnet.puts( PacketHandler.packet_upload_config(name, JSON.dump( config )) )
end
else
if $server.active_client && $server.active_client.connected?
json = File.read(TAC::CONFIGS_PATH)
$server.active_client.puts(PacketHandler.packet_upload_config(json))
$server.active_client.puts(PacketHandler.packet_list_configs)
end
end
end
def handle_select_config(packet)
config_name = packet.body
$window.backend.settings.config = config_name
$window.backend.save_settings
$window.backend.load_config(config_name)
end
def handle_add_config(packet)
config_name = packet.body
if $window.backend.configs_list.include?(config_name)
unless @host_is_a_connection
if $server.active_client&.connected?
$server.active_client.puts(PacketHandler.packet_error("Config already exists!", "A config with the name #{config_name} already exists over here."))
end
end
else
$window.backend.write_new_config(config_name)
end
end
def handle_update_config(packet)
old_config_name, new_config_name = packet.body.split(PROTOCOL_SEPERATOR, 2)
if $window.backend.configs_list.include?(config_name)
unless @host_is_a_connection
if $server.active_client&.connected?
$server.active_client.puts(PacketHandler.packet_error("Config already exists!", "A config with the name #{config_name} already exists over here."))
end
end
else
$window.backend.move_config(old_config_name, new_config_name)
end
end
def handle_delete_config(packet)
config_name = packet.body
$window.backend.delete_config(config_name)
end
def self.packet_handshake(client_uuid)
Packet.create(Packet::PACKET_TYPES[:handshake], client_uuid)
end
@@ -95,15 +191,41 @@ module TAC
Packet.create(Packet::PACKET_TYPES[:heartbeat], Packet::PROTOCOL_HEARTBEAT)
end
def self.packet_download_config
Packet.create(Packet::PACKET_TYPES[:download_config], "")
def self.packet_error(error_code, message)
Packet.create(Packet::PACKET_TYPES[:error], error_code.to_s, message.to_s)
end
def self.packet_upload_config(string)
string = string.gsub("\n", " ")
def self.packet_download_config(config_name)
Packet.create(Packet::PACKET_TYPES[:download_config], "#{config_name}")
end
def self.packet_upload_config(config_name, json)
string = "#{config_name}#{Packet::PROTOCOL_SEPERATOR}#{json.gsub("\n", " ")}"
Packet.create(Packet::PACKET_TYPES[:upload_config], string)
end
def self.packet_list_configs
files = Dir.glob("#{TAC::CONFIGS_PATH}/*.json")
list = files.map do |file|
name = File.basename(file, ".json")
config = Config.new(name)
"#{name},#{config.configuration.revision}"
end.join(Packet::PROTOCOL_SEPERATOR)
Packet.create(
Packet::PACKET_TYPES[:list_configs],
list
)
end
def self.packet_select_config(config_name)
Packet.create(Packet::PACKET_TYPES[:select_config], config_name)
end
def self.packet_delete_config(config_name)
Packet.create(Packet::PACKET_TYPES[:delete_config], config_name)
end
end
end
end
end

View File

@@ -71,10 +71,9 @@ module TAC
@active_client = client
# TODO: Backup local config
# SEND CONFIG
config = File.read(TAC::CONFIGS_PATH)
@active_client.puts(PacketHandler.packet_handshake(@active_client.uuid))
@active_client.puts(PacketHandler.packet_upload_config(config))
@active_client.puts(PacketHandler.packet_list_configs)
log.i(TAG, "Client connected!")
@@ -136,4 +135,4 @@ module TAC
end
end
end
end
end

View File

@@ -1,11 +1,13 @@
module TAC
THEME_FONT = "#{TAC::ROOT_PATH}/media/fonts/DejaVuSansCondensed.ttf"
THEME = {
Label: {
font: "#{TAC::ROOT_PATH}/media/fonts/DejaVuSansCondensed.ttf",
text_size: 18,
font: THEME_FONT,
text_size: 22,
color: Gosu::Color.new(0xee_ffffff),
},
Button: {
text_size: 22,
background: TAC::Palette::TIMECRAFTERS_PRIMARY,
border_thickness: 1,
border_color: Gosu::Color.new(0xff_111111),
@@ -17,7 +19,7 @@ module TAC
}
},
EditLine: {
caret_color: Gosu::Color.new(0xff_434343),
caret_color: Gosu::Color.new(0xff_88ef90),
},
ToggleButton: {
width: 18,
@@ -36,7 +38,8 @@ module TAC
}
}
THEME_ICON_SIZE = 18
THEME_DIALOG_BUTTON_PADDING = 24
THEME_ICON_SIZE = 22
THEME_HEADING_TEXT_SIZE = 32
THEME_SUBHEADING_TEXT_SIZE = 28
THEME_ITEM_PADDING = 8
@@ -46,11 +49,15 @@ module TAC
padding_top: THEME_ITEM_PADDING,
padding_bottom: THEME_ITEM_PADDING
}
THEME_EVEN_COLOR = Gosu::Color.new(0xff_606060)
THEME_ODD_COLOR = Gosu::Color.new(0xff_202020)
THEME_EVEN_COLOR = Gosu::Color.new(0xff_202020)
THEME_ODD_COLOR = Gosu::Color.new(0xff_606060)
THEME_CONTENT_BACKGROUND = Gosu::Color.new(0x88_007f3f)
THEME_HEADER_BACKGROUND = [
TAC::Palette::TIMECRAFTERS_PRIMARY, TAC::Palette::TIMECRAFTERS_PRIMARY,
TAC::Palette::TIMECRAFTERS_SECONDARY, TAC::Palette::TIMECRAFTERS_SECONDARY,
]
THEME_NOTIFICATION_EDGE_COLOR = Gosu::Color.new(0xff_008000)
THEME_NOTIFICATION_BACKGROUND = Gosu::Color.new(0xff_102010)
THEME_NOTIFICATION_TITLE_COLOR = Gosu::Color::WHITE
THEME_NOTIFICATION_TAGLINE_COLOR = Gosu::Color::WHITE
end

View File

@@ -1,5 +1,5 @@
module TAC
NAME = "TimeCrafters Configuration Tool"
VERSION = "0.1.0"
RELEASE_NAME = "IN-DEV"
VERSION = "0.5.1"
RELEASE_NAME = "Beta"
end

View File

@@ -1,23 +1,72 @@
module TAC
class Window < CyberarmEngine::Window
attr_reader :backend
attr_reader :backend, :notification_manager
def initialize(**args)
super(**args)
self.caption = "#{TAC::NAME} v#{TAC::VERSION} (#{TAC::RELEASE_NAME})"
@backend = Backend.new
push_state(TAC::States::Boot)
@notification_manager = GosuNotifications::NotificationManager.new(window: self, edge: :bottom)
if ARGV.join.include?("--game-clock-remote-display")
push_state(PracticeGameClock::View, remote_control_mode: true)
else
push_state(TAC::States::Boot)
end
end
def needs_cursor?
true
def draw
super
Gosu.flush
@notification_manager.draw
end
def update
super
@notification_manager.update
end
def toast(title, message = nil)
@notification_manager.create_notification(
priority: GosuNotifications::Notification::PRIORITY_HIGH,
title: title,
tagline: message ? message : "",
edge_color: THEME_NOTIFICATION_EDGE_COLOR,
background_color: THEME_NOTIFICATION_BACKGROUND,
title_color: THEME_NOTIFICATION_TITLE_COLOR,
tagline_color: THEME_NOTIFICATION_TAGLINE_COLOR
)
end
def hit_test(x, y)
return 0 unless BORDERLESS
if y <= 4
return 2 if x <= 4
return 4 if x >= width - 4
return 3
end
if y >= height - 4
return 8 if x <= 4
return 6 if x >= width - 4
return 7
end
return 1 if y <= 36 && x <= width - (36 * 3 + 4 * 6)
0
end
def close
if @backend.config_changed?
push_state(Dialog::ConfirmDialog, title: "Unsaved Config", message: "Config has unsaved changes\nthat will be lost if\nyou continue!", callback_method: proc { cleanup_and_close })
push_state(Dialog::ConfirmDialog, title: "Unsaved Config", message: "Config has unsaved changes that will be lost if you continue!", callback_method: proc { cleanup_and_close })
elsif @backend.settings_changed?
push_state(Dialog::ConfirmDialog, title: "Unsaved Settings", message: "Settings has unsaved changes\nthat will be lost if\nyou continue!", callback_method: proc { cleanup_and_close })
push_state(Dialog::ConfirmDialog, title: "Unsaved Settings", message: "Settings has unsaved changes that will be lost if you continue!", callback_method: proc { cleanup_and_close })
else
cleanup_and_close
end
@@ -31,4 +80,4 @@ module TAC
close!
end
end
end
end

BIN
media/background.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 134 KiB

6244
media/background.svg Normal file

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 231 KiB

BIN
media/error_alarm.ogg Normal file

Binary file not shown.

BIN
media/icon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@@ -0,0 +1,117 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="100"
height="100"
viewBox="0 0 100 100"
version="1.1"
id="svg5"
inkscape:export-filename="/home/cyberarm/Code/timecrafters_configuration_tool_desktop/media/icons/custom_stopWatch.png"
inkscape:export-xdpi="96"
inkscape:export-ydpi="96"
inkscape:version="1.1.1 (3bf5ae0d25, 2021-09-20)"
sodipodi:docname="custom_stopWatch.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview7"
pagecolor="#dcdcdc"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageshadow="2"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="0"
inkscape:document-units="px"
showgrid="true"
inkscape:showpageshadow="false"
borderlayer="true"
inkscape:snap-bbox="true"
inkscape:snap-bbox-midpoints="true"
inkscape:bbox-nodes="true"
inkscape:zoom="4"
inkscape:cx="-12.625"
inkscape:cy="52.625"
inkscape:window-width="1920"
inkscape:window-height="1011"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="layer1">
<inkscape:grid
type="xygrid"
id="grid824" />
</sodipodi:namedview>
<defs
id="defs2" />
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1">
<rect
style="fill:#ffffff;fill-opacity:0.98396;stroke:none;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none"
id="rect3345"
width="20"
height="6"
x="40"
y="7"
ry="1" />
<path
id="path848"
style="fill:#ffffff;fill-opacity:0.98396;stroke-width:2"
d="M 50 15 A 35 35 0 0 0 15 50 A 35 35 0 0 0 50 85 A 35 35 0 0 0 85 50 A 35 35 0 0 0 50 15 z M 50 25 A 25.000003 25.000003 0 0 1 75 50 A 25.000003 25.000003 0 0 1 50 75 A 25.000003 25.000003 0 0 1 25 50 A 25.000003 25.000003 0 0 1 50 25 z " />
<circle
style="fill:#ffffff;fill-opacity:0.98396;stroke-width:2"
id="path1762"
cx="50"
cy="50"
r="2" />
<path
style="fill:none;stroke:#ffffff;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;stroke-miterlimit:4;stroke-dasharray:none"
d="M 50,50 60,30"
id="path2248"
sodipodi:nodetypes="cc" />
<path
style="fill:none;stroke:#ffffff;stroke-width:4;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 50,26 v 9"
id="path2778"
sodipodi:nodetypes="cc" />
<path
style="fill:none;stroke:#ffffff;stroke-width:4;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 50,65 v 9"
id="path2778-0"
sodipodi:nodetypes="cc" />
<path
style="fill:none;stroke:#ffffff;stroke-width:4;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M 74,50 H 65"
id="path2778-9"
sodipodi:nodetypes="cc" />
<path
style="fill:none;stroke:#ffffff;stroke-width:4;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M 35,50 H 26"
id="path2778-0-3"
sodipodi:nodetypes="cc" />
<path
style="fill:none;stroke:#ffffff;stroke-width:4;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 66.970563,33.029437 -6.363961,6.363961"
id="path2778-6"
sodipodi:nodetypes="cc" />
<path
style="fill:none;stroke:#ffffff;stroke-width:4;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 39.393398,60.606602 -6.363961,6.363961"
id="path2778-0-0"
sodipodi:nodetypes="cc" />
<path
style="fill:none;stroke:#ffffff;stroke-width:4;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M 66.970563,66.970563 60.606602,60.606602"
id="path2778-9-6"
sodipodi:nodetypes="cc" />
<path
style="fill:none;stroke:#ffffff;stroke-width:4;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M 39.393398,39.393398 33.029437,33.029437"
id="path2778-0-3-2"
sodipodi:nodetypes="cc" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.6 KiB

0
media/music/.gitkeep Normal file
View File

BIN
media/openclipart_ducky.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

0
media/particles/.gitkeep Normal file
View File

0
media/sounds/.gitkeep Normal file
View File

View File

@@ -4,6 +4,8 @@ require "securerandom"
require_relative "lib/tac"
require_relative "lib/logger"
require_relative "lib/settings"
require_relative "lib/config"
require_relative "lib/tacnet"
require_relative "lib/tacnet/packet"

View File

@@ -1,4 +1,10 @@
require_relative "../cyberarm_engine/lib/cyberarm_engine"
begin
raise LoadError if defined?(Ocra)
require_relative "../cyberarm_engine/lib/cyberarm_engine"
rescue LoadError
require "cyberarm_engine"
end
require "gosu_notifications"
require "socket"
require "securerandom"
require "json"
@@ -13,9 +19,17 @@ require_relative "lib/config"
require_relative "lib/settings"
require_relative "lib/states/boot"
require_relative "lib/states/editor"
require_relative "lib/states/simulator"
require_relative "lib/states/manage_presets"
require_relative "lib/states/manage_configurations"
require_relative "lib/page"
require_relative "lib/pages/home"
require_relative "lib/pages/editor"
require_relative "lib/pages/tacnet"
require_relative "lib/pages/simulator"
require_relative "lib/pages/configurations"
require_relative "lib/pages/presets"
require_relative "lib/pages/search"
require_relative "lib/pages/field_planner"
require_relative "lib/pages/drive_team_rotation_generator"
require_relative "lib/pages/game_clock"
require_relative "lib/simulator/robot"
require_relative "lib/simulator/field"
require_relative "lib/simulator/simulation"
@@ -25,7 +39,11 @@ require_relative "lib/dialog"
require_relative "lib/dialogs/alert_dialog"
require_relative "lib/dialogs/confirm_dialog"
require_relative "lib/dialogs/name_prompt_dialog"
require_relative "lib/dialogs/action_dialog"
require_relative "lib/dialogs/variable_dialog"
require_relative "lib/dialogs/tacnet_dialog"
require_relative "lib/dialogs/tacnet_status_dialog"
require_relative "lib/dialogs/pick_preset_dialog"
require_relative "lib/tacnet"
require_relative "lib/tacnet/packet"
require_relative "lib/tacnet/packet_handler"
@@ -33,6 +51,31 @@ require_relative "lib/tacnet/client"
require_relative "lib/tacnet/connection"
require_relative "lib/tacnet/server"
require_relative "lib/game_clock/view"
require_relative "lib/game_clock/clock"
require_relative "lib/game_clock/event_handlers"
require_relative "lib/game_clock/clock_controller"
require_relative "lib/game_clock/jukebox"
require_relative "lib/game_clock/theme"
require_relative "lib/game_clock/clock_proxy"
require_relative "lib/game_clock/logger"
require_relative "lib/game_clock/particle_emitter"
require_relative "lib/game_clock/randomizer"
require_relative "lib/game_clock/remote_control"
require_relative "lib/game_clock/remote_proxy"
require_relative "lib/game_clock/net/client"
require_relative "lib/game_clock/net/server"
require_relative "lib/game_clock/net/connection"
require_relative "lib/game_clock/net/packet_handler"
require_relative "lib/game_clock/net/packet"
# Thread.abort_on_exception = true
TAC::Window.new(width: (Gosu.screen_width * 0.8).round, height: (Gosu.screen_height * 0.8).round, resizable: true).show
USE_REDESIGN = ARGV.include?("--redesign")
BORDERLESS = ARGV.include?("--borderless")
if not defined?(Ocra)
TAC::Window.new(width: (Gosu.screen_width * 0.8).round, height: (Gosu.screen_height * 0.8).round, resizable: true, borderless: BORDERLESS).show
end
Process.wait($clock_pid) if $clock_pid