cyberarm_engine 0.13.0 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +8 -8
  3. data/.rubocop.yml +8 -0
  4. data/.travis.yml +5 -5
  5. data/Gemfile +6 -6
  6. data/LICENSE.txt +21 -21
  7. data/README.md +73 -43
  8. data/Rakefile +10 -10
  9. data/assets/textures/default.png +0 -0
  10. data/bin/console +14 -14
  11. data/bin/setup +8 -8
  12. data/cyberarm_engine.gemspec +39 -36
  13. data/lib/cyberarm_engine.rb +64 -47
  14. data/lib/cyberarm_engine/animator.rb +56 -54
  15. data/lib/cyberarm_engine/background.rb +179 -175
  16. data/lib/cyberarm_engine/background_nine_slice.rb +125 -0
  17. data/lib/cyberarm_engine/bounding_box.rb +150 -150
  18. data/lib/cyberarm_engine/cache.rb +4 -0
  19. data/lib/cyberarm_engine/cache/download_manager.rb +121 -0
  20. data/lib/cyberarm_engine/common.rb +96 -96
  21. data/lib/cyberarm_engine/config_file.rb +46 -0
  22. data/lib/cyberarm_engine/game_object.rb +248 -257
  23. data/lib/cyberarm_engine/game_state.rb +92 -89
  24. data/lib/cyberarm_engine/model.rb +207 -0
  25. data/lib/cyberarm_engine/model/material.rb +21 -0
  26. data/lib/cyberarm_engine/model/model_object.rb +131 -0
  27. data/lib/cyberarm_engine/model/parser.rb +74 -0
  28. data/lib/cyberarm_engine/model/parsers/collada_parser.rb +138 -0
  29. data/lib/cyberarm_engine/model/parsers/wavefront_parser.rb +154 -0
  30. data/lib/cyberarm_engine/model_cache.rb +31 -0
  31. data/lib/cyberarm_engine/opengl.rb +28 -0
  32. data/lib/cyberarm_engine/opengl/light.rb +50 -0
  33. data/lib/cyberarm_engine/opengl/orthographic_camera.rb +46 -0
  34. data/lib/cyberarm_engine/opengl/perspective_camera.rb +38 -0
  35. data/lib/cyberarm_engine/opengl/renderer/bounding_box_renderer.rb +249 -0
  36. data/lib/cyberarm_engine/opengl/renderer/g_buffer.rb +164 -0
  37. data/lib/cyberarm_engine/opengl/renderer/opengl_renderer.rb +289 -0
  38. data/lib/cyberarm_engine/opengl/renderer/renderer.rb +22 -0
  39. data/lib/cyberarm_engine/opengl/shader.rb +406 -0
  40. data/lib/cyberarm_engine/opengl/texture.rb +69 -0
  41. data/lib/cyberarm_engine/ray.rb +56 -56
  42. data/lib/cyberarm_engine/stats.rb +21 -0
  43. data/lib/cyberarm_engine/text.rb +160 -146
  44. data/lib/cyberarm_engine/timer.rb +23 -23
  45. data/lib/cyberarm_engine/transform.rb +296 -273
  46. data/lib/cyberarm_engine/ui/border_canvas.rb +102 -101
  47. data/lib/cyberarm_engine/ui/dsl.rb +138 -99
  48. data/lib/cyberarm_engine/ui/element.rb +315 -276
  49. data/lib/cyberarm_engine/ui/elements/button.rb +160 -67
  50. data/lib/cyberarm_engine/ui/elements/check_box.rb +51 -59
  51. data/lib/cyberarm_engine/ui/elements/container.rb +256 -176
  52. data/lib/cyberarm_engine/ui/elements/edit_box.rb +179 -0
  53. data/lib/cyberarm_engine/ui/elements/edit_line.rb +262 -172
  54. data/lib/cyberarm_engine/ui/elements/flow.rb +15 -17
  55. data/lib/cyberarm_engine/ui/elements/image.rb +72 -52
  56. data/lib/cyberarm_engine/ui/elements/label.rb +156 -50
  57. data/lib/cyberarm_engine/ui/elements/list_box.rb +82 -0
  58. data/lib/cyberarm_engine/ui/elements/progress.rb +51 -50
  59. data/lib/cyberarm_engine/ui/elements/radio.rb +6 -0
  60. data/lib/cyberarm_engine/ui/elements/slider.rb +104 -0
  61. data/lib/cyberarm_engine/ui/elements/stack.rb +11 -13
  62. data/lib/cyberarm_engine/ui/elements/text_block.rb +156 -0
  63. data/lib/cyberarm_engine/ui/elements/toggle_button.rb +65 -56
  64. data/lib/cyberarm_engine/ui/event.rb +47 -47
  65. data/lib/cyberarm_engine/ui/gui_state.rb +226 -135
  66. data/lib/cyberarm_engine/ui/style.rb +38 -37
  67. data/lib/cyberarm_engine/ui/theme.rb +182 -120
  68. data/lib/cyberarm_engine/vector.rb +293 -203
  69. data/lib/cyberarm_engine/version.rb +4 -4
  70. data/lib/cyberarm_engine/{engine.rb → window.rb} +114 -101
  71. metadata +88 -18
  72. data/lib/cyberarm_engine/gosu_ext/circle.rb +0 -9
  73. data/lib/cyberarm_engine/shader.rb +0 -262
@@ -0,0 +1,125 @@
1
+ module CyberarmEngine
2
+ class BackgroundNineSlice
3
+ include CyberarmEngine::Common
4
+ attr_accessor :x, :y, :z, :width, :height
5
+
6
+ def initialize(image_path:, x: 0, y: 0, z: 0, width: 64, height: 64, mode: :tiled, left: 4, top: 4, right: 56, bottom: 56)
7
+ @image = get_image(image_path)
8
+
9
+ @x = x
10
+ @y = y
11
+ @z = z
12
+
13
+ @width = width
14
+ @height = height
15
+
16
+ @mode = mode
17
+
18
+ @left = left
19
+ @top = top
20
+ @right = right
21
+ @bottom = bottom
22
+
23
+ nine_slice
24
+ end
25
+
26
+ def nine_slice
27
+ @segment_top_left = Gosu.render(@left, @top) { @image.draw(0, 0, 0) }
28
+ @segment_top_right = Gosu.render(@image.width - @right, @top) { @image.draw(-@right, 0, 0) }
29
+
30
+ @segment_left = Gosu.render(@left, @bottom - @top) { @image.draw(0, -@top, 0) }
31
+ @segment_right = Gosu.render(@image.width - @right, @bottom - @top) { @image.draw(-@right, -@top, 0) }
32
+
33
+ @segment_bottom_left = Gosu.render(@left, @image.height - @bottom) { @image.draw(0, -@bottom, 0) }
34
+ @segment_bottom_right = Gosu.render(@image.width - @right, @image.height - @bottom) { @image.draw(-@right, -@bottom, 0) }
35
+
36
+ @segment_top = Gosu.render(@right - @left, @top) { @image.draw(-@left, 0, 0) }
37
+ @segment_bottom = Gosu.render(@right - @left, @image.height - @bottom) { @image.draw(-@left, -@bottom, 0) }
38
+
39
+ @segment_middle = Gosu.render(@right - @left, @bottom - @top) { @image.draw(-@left, -@top, 0) }
40
+ end
41
+
42
+ def cx
43
+ @x + @left
44
+ end
45
+
46
+ def cy
47
+ @y + @top
48
+ end
49
+
50
+ def cwidth
51
+ @cx - @width
52
+ end
53
+
54
+ def cheight
55
+ @cy - @height
56
+ end
57
+
58
+ def width_scale
59
+ width_scale = (@width - (@left + (@image.width - @right))).to_f / (@right - @left)
60
+ end
61
+
62
+ def height_scale
63
+ height_scale = (@height - (@top + (@image.height - @bottom))).to_f / (@bottom - @top)
64
+ end
65
+
66
+ def draw
67
+ @mode == :tiled ? draw_tiled : draw_stretched
68
+ end
69
+
70
+ def draw_stretched
71
+ @segment_top_left.draw(@x, @y, @z)
72
+ @segment_top.draw(@x + @segment_top_left.width, @y, @z, width_scale) # SCALE X
73
+ @segment_top_right.draw((@x + @width) - @segment_top_right.width, @y, @z)
74
+
75
+ @segment_right.draw((@x + @width) - @segment_right.width, @y + @top, @z, 1, height_scale) # SCALE Y
76
+ @segment_bottom_right.draw((@x + @width) - @segment_bottom_right.width, @y + @height - @segment_bottom_right.height, @z)
77
+ @segment_bottom.draw(@x + @segment_bottom_left.width, (@y + @height) - @segment_bottom.height, @z, width_scale) # SCALE X
78
+ @segment_bottom_left.draw(@x, (@y + @height) - @segment_bottom_left.height, @z)
79
+ @segment_left.draw(@x, @y + @top, @z, 1, height_scale) # SCALE Y
80
+ @segment_middle.draw(@x + @segment_top_left.width, @y + @segment_top.height, @z, width_scale, height_scale) # SCALE X and SCALE Y
81
+ end
82
+
83
+ def draw_tiled
84
+ @segment_top_left.draw(@x, @y, @z)
85
+
86
+ Gosu.clip_to(@x + @segment_top_left.width, @y, @segment_top.width * width_scale, @segment_top.height) do
87
+ width_scale.ceil.times do |i|
88
+ @segment_top.draw(@x + @segment_top_left.width + (@segment_top.width * i), @y, @z) # SCALE X
89
+ end
90
+ end
91
+
92
+ @segment_top_right.draw((@x + @width) - @segment_top_right.width, @y, @z)
93
+
94
+ Gosu.clip_to(@x + @width - @segment_top_right.width, @y + @top, @segment_right.width, @segment_right.height * height_scale) do
95
+ height_scale.ceil.times do |i|
96
+ @segment_right.draw((@x + @width) - @segment_right.width, @y + @top + (@segment_right.height * i), @z) # SCALE Y
97
+ end
98
+ end
99
+
100
+ @segment_bottom_right.draw((@x + @width) - @segment_bottom_right.width, @y + @height - @segment_bottom_right.height, @z)
101
+
102
+ Gosu.clip_to(@x + @segment_top_left.width, @y + @height - @segment_bottom.height, @segment_top.width * width_scale, @segment_bottom.height) do
103
+ width_scale.ceil.times do |i|
104
+ @segment_bottom.draw(@x + @segment_bottom_left.width + (@segment_bottom.width * i), (@y + @height) - @segment_bottom.height, @z) # SCALE X
105
+ end
106
+ end
107
+
108
+ @segment_bottom_left.draw(@x, (@y + @height) - @segment_bottom_left.height, @z)
109
+
110
+ Gosu.clip_to(@x, @y + @top, @segment_left.width, @segment_left.height * height_scale) do
111
+ height_scale.ceil.times do |i|
112
+ @segment_left.draw(@x, @y + @top + (@segment_left.height * i), @z) # SCALE Y
113
+ end
114
+ end
115
+
116
+ Gosu.clip_to(@x + @segment_top_left.width, @y + @segment_top.height, @width - (@segment_left.width + @segment_right.width), @height - (@segment_top.height + @segment_bottom.height)) do
117
+ height_scale.ceil.times do |y|
118
+ width_scale.ceil.times do |x|
119
+ @segment_middle.draw(@x + @segment_top_left.width + (@segment_middle.width * x), @y + @segment_top.height + (@segment_middle.height * y), @z) # SCALE X and SCALE Y
120
+ end
121
+ end
122
+ end
123
+ end
124
+ end
125
+ end
@@ -1,150 +1,150 @@
1
- module CyberarmEngine
2
- class BoundingBox
3
- attr_accessor :min, :max
4
-
5
- def initialize(*args)
6
- case args.size
7
- when 0
8
- @min = Vector.new(0, 0, 0)
9
- @max = Vector.new(0, 0, 0)
10
- when 2
11
- @min = args.first.clone
12
- @max = args.last.clone
13
- when 4
14
- @min = Vector.new(args[0], args[1], 0)
15
- @max = Vector.new(args[2], args[3], 0)
16
- when 6
17
- @min = Vector.new(args[0], args[1], args[2])
18
- @max = Vector.new(args[3], args[4], args[5])
19
- else
20
- raise "Invalid number of arguments! Got: #{args.size}, expected: 0, 2, 4, or 6."
21
- end
22
- end
23
-
24
- def ==(other)
25
- @min == other.min &&
26
- @max == other.max
27
- end
28
-
29
- # returns a new bounding box that includes both bounding boxes
30
- def union(other)
31
- temp = BoundingBox.new
32
- temp.min.x = [@min.x, other.min.x].min
33
- temp.min.y = [@min.y, other.min.y].min
34
- temp.min.z = [@min.z, other.min.z].min
35
-
36
- temp.max.x = [@max.x, other.max.x].max
37
- temp.max.y = [@max.y, other.max.y].max
38
- temp.max.z = [@max.z, other.max.z].max
39
-
40
- return temp
41
- end
42
-
43
- # returns the difference between both bounding boxes
44
- def difference(other)
45
- temp = BoundingBox.new
46
- temp.min = @min - other.min
47
- temp.max = @max - other.max
48
-
49
- return temp
50
- end
51
-
52
- # returns whether bounding box intersects other
53
- def intersect?(other)
54
- if other.is_a?(Ray)
55
- other.intersect?(self)
56
- elsif other.is_a?(BoundingBox)
57
- (@min.x <= other.max.x && @max.x >= other.min.x) &&
58
- (@min.y <= other.max.y && @max.y >= other.min.y) &&
59
- (@min.z <= other.max.z && @max.z >= other.min.z)
60
- else
61
- raise "Unknown collider: #{other.class}"
62
- end
63
- end
64
-
65
- # does this bounding box envelop other bounding box? (inclusive of border)
66
- def contains?(other)
67
- other.min.x >= min.x && other.min.y >= min.y && other.min.z >= min.z &&
68
- other.max.x <= max.x && other.max.y <= max.y && other.max.z <= max.z
69
- end
70
-
71
- # returns whether the 3D vector is inside of the bounding box
72
- def inside?(vector)
73
- (vector.x.between?(@min.x, @max.x) || vector.x.between?(@max.x, @min.x)) &&
74
- (vector.y.between?(@min.y, @max.y) || vector.y.between?(@max.y, @min.y)) &&
75
- (vector.z.between?(@min.z, @max.z) || vector.z.between?(@max.z, @min.z))
76
- end
77
-
78
- # returns whether the 2D vector is inside of the bounding box
79
- def point?(vector)
80
- (vector.x.between?(@min.x, @max.x) || vector.x.between?(@max.x, @min.x)) &&
81
- (vector.y.between?(@min.y, @max.y) || vector.y.between?(@max.y, @min.y))
82
- end
83
-
84
- def volume
85
- width * height * depth
86
- end
87
-
88
- def width
89
- @max.x - @min.x
90
- end
91
-
92
- def height
93
- @max.y - @min.y
94
- end
95
-
96
- def depth
97
- @max.z - @min.z
98
- end
99
-
100
- def normalize(entity)
101
- temp = BoundingBox.new
102
- temp.min.x = @min.x.to_f * entity.scale.x
103
- temp.min.y = @min.y.to_f * entity.scale.y
104
- temp.min.z = @min.z.to_f * entity.scale.z
105
-
106
- temp.max.x = @max.x.to_f * entity.scale.x
107
- temp.max.y = @max.y.to_f * entity.scale.y
108
- temp.max.z = @max.z.to_f * entity.scale.z
109
-
110
- return temp
111
- end
112
-
113
- def normalize_with_offset(entity)
114
- temp = BoundingBox.new
115
- temp.min.x = @min.x.to_f * entity.scale.x + entity.position.x
116
- temp.min.y = @min.y.to_f * entity.scale.y + entity.position.y
117
- temp.min.z = @min.z.to_f * entity.scale.z + entity.position.z
118
-
119
- temp.max.x = @max.x.to_f * entity.scale.x + entity.position.x
120
- temp.max.y = @max.y.to_f * entity.scale.y + entity.position.y
121
- temp.max.z = @max.z.to_f * entity.scale.z + entity.position.z
122
-
123
- return temp
124
- end
125
-
126
- def +(other)
127
- box = BoundingBox.new
128
- box.min = self.min + other.min
129
- box.min = self.max + other.max
130
-
131
- return box
132
- end
133
-
134
- def -(other)
135
- box = BoundingBox.new
136
- box.min = self.min - other.min
137
- box.min = self.max - other.max
138
-
139
- return box
140
- end
141
-
142
- def sum
143
- @min.sum + @max.sum
144
- end
145
-
146
- def clone
147
- BoundingBox.new(@min.x, @min.y, @min.z, @max.x, @max.y, @max.z)
148
- end
149
- end
150
- end
1
+ module CyberarmEngine
2
+ class BoundingBox
3
+ attr_accessor :min, :max
4
+
5
+ def initialize(*args)
6
+ case args.size
7
+ when 0
8
+ @min = Vector.new(0, 0, 0)
9
+ @max = Vector.new(0, 0, 0)
10
+ when 2
11
+ @min = args.first.clone
12
+ @max = args.last.clone
13
+ when 4
14
+ @min = Vector.new(args[0], args[1], 0)
15
+ @max = Vector.new(args[2], args[3], 0)
16
+ when 6
17
+ @min = Vector.new(args[0], args[1], args[2])
18
+ @max = Vector.new(args[3], args[4], args[5])
19
+ else
20
+ raise "Invalid number of arguments! Got: #{args.size}, expected: 0, 2, 4, or 6."
21
+ end
22
+ end
23
+
24
+ def ==(other)
25
+ @min == other.min &&
26
+ @max == other.max
27
+ end
28
+
29
+ # returns a new bounding box that includes both bounding boxes
30
+ def union(other)
31
+ temp = BoundingBox.new
32
+ temp.min.x = [@min.x, other.min.x].min
33
+ temp.min.y = [@min.y, other.min.y].min
34
+ temp.min.z = [@min.z, other.min.z].min
35
+
36
+ temp.max.x = [@max.x, other.max.x].max
37
+ temp.max.y = [@max.y, other.max.y].max
38
+ temp.max.z = [@max.z, other.max.z].max
39
+
40
+ temp
41
+ end
42
+
43
+ # returns the difference between both bounding boxes
44
+ def difference(other)
45
+ temp = BoundingBox.new
46
+ temp.min = @min - other.min
47
+ temp.max = @max - other.max
48
+
49
+ temp
50
+ end
51
+
52
+ # returns whether bounding box intersects other
53
+ def intersect?(other)
54
+ if other.is_a?(Ray)
55
+ other.intersect?(self)
56
+ elsif other.is_a?(BoundingBox)
57
+ (@min.x <= other.max.x && @max.x >= other.min.x) &&
58
+ (@min.y <= other.max.y && @max.y >= other.min.y) &&
59
+ (@min.z <= other.max.z && @max.z >= other.min.z)
60
+ else
61
+ raise "Unknown collider: #{other.class}"
62
+ end
63
+ end
64
+
65
+ # does this bounding box envelop other bounding box? (inclusive of border)
66
+ def contains?(other)
67
+ other.min.x >= min.x && other.min.y >= min.y && other.min.z >= min.z &&
68
+ other.max.x <= max.x && other.max.y <= max.y && other.max.z <= max.z
69
+ end
70
+
71
+ # returns whether the 3D vector is inside of the bounding box
72
+ def inside?(vector)
73
+ (vector.x.between?(@min.x, @max.x) || vector.x.between?(@max.x, @min.x)) &&
74
+ (vector.y.between?(@min.y, @max.y) || vector.y.between?(@max.y, @min.y)) &&
75
+ (vector.z.between?(@min.z, @max.z) || vector.z.between?(@max.z, @min.z))
76
+ end
77
+
78
+ # returns whether the 2D vector is inside of the bounding box
79
+ def point?(vector)
80
+ (vector.x.between?(@min.x, @max.x) || vector.x.between?(@max.x, @min.x)) &&
81
+ (vector.y.between?(@min.y, @max.y) || vector.y.between?(@max.y, @min.y))
82
+ end
83
+
84
+ def volume
85
+ width * height * depth
86
+ end
87
+
88
+ def width
89
+ @max.x - @min.x
90
+ end
91
+
92
+ def height
93
+ @max.y - @min.y
94
+ end
95
+
96
+ def depth
97
+ @max.z - @min.z
98
+ end
99
+
100
+ def normalize(entity)
101
+ temp = BoundingBox.new
102
+ temp.min.x = @min.x.to_f * entity.scale.x
103
+ temp.min.y = @min.y.to_f * entity.scale.y
104
+ temp.min.z = @min.z.to_f * entity.scale.z
105
+
106
+ temp.max.x = @max.x.to_f * entity.scale.x
107
+ temp.max.y = @max.y.to_f * entity.scale.y
108
+ temp.max.z = @max.z.to_f * entity.scale.z
109
+
110
+ temp
111
+ end
112
+
113
+ def normalize_with_offset(entity)
114
+ temp = BoundingBox.new
115
+ temp.min.x = @min.x.to_f * entity.scale.x + entity.position.x
116
+ temp.min.y = @min.y.to_f * entity.scale.y + entity.position.y
117
+ temp.min.z = @min.z.to_f * entity.scale.z + entity.position.z
118
+
119
+ temp.max.x = @max.x.to_f * entity.scale.x + entity.position.x
120
+ temp.max.y = @max.y.to_f * entity.scale.y + entity.position.y
121
+ temp.max.z = @max.z.to_f * entity.scale.z + entity.position.z
122
+
123
+ temp
124
+ end
125
+
126
+ def +(other)
127
+ box = BoundingBox.new
128
+ box.min = min + other.min
129
+ box.min = max + other.max
130
+
131
+ box
132
+ end
133
+
134
+ def -(other)
135
+ box = BoundingBox.new
136
+ box.min = min - other.min
137
+ box.min = max - other.max
138
+
139
+ box
140
+ end
141
+
142
+ def sum
143
+ @min.sum + @max.sum
144
+ end
145
+
146
+ def clone
147
+ BoundingBox.new(@min.x, @min.y, @min.z, @max.x, @max.y, @max.z)
148
+ end
149
+ end
150
+ end
@@ -0,0 +1,4 @@
1
+ module CyberarmEngine
2
+ module Cache
3
+ end
4
+ end
@@ -0,0 +1,121 @@
1
+ module CyberarmEngine
2
+ module Cache
3
+ class DownloadManager
4
+ attr_reader :downloads
5
+
6
+ def initialize(max_parallel_downloads: 4)
7
+ @max_parallel_downloads = max_parallel_downloads
8
+ @downloads = []
9
+ end
10
+
11
+ def download(url:, save_as: nil, &callback)
12
+ uri = URI(url)
13
+ save_as ||= "filename_path" # TODO: if no save_as path is provided, then get one from the Cache controller
14
+
15
+ @downloads << Download.new(uri: uri, save_as: save_as, callback: callback)
16
+ end
17
+
18
+ def status
19
+ if active_downloads > 0
20
+ :busy
21
+ else
22
+ :idle
23
+ end
24
+ end
25
+
26
+ def progress
27
+ remaining_bytes = @downloads.map { |d| d.remaining_bytes }.sum
28
+ total_bytes = @downloads.map { |d| d.total_bytes }.sum
29
+
30
+ v = 1.0 - (remaining_bytes.to_f / total_bytes)
31
+ return 0.0 if v.nan?
32
+
33
+ v
34
+ end
35
+
36
+ def active_downloads
37
+ @downloads.select { |d| %i[pending downloading].include?(d.status) }
38
+ end
39
+
40
+ def update
41
+ @downloads.each do |download|
42
+ if download.status == :pending && active_downloads.size <= @max_parallel_downloads
43
+ download.status = :downloading
44
+ Thread.start { download.download }
45
+ end
46
+ end
47
+ end
48
+
49
+ def prune
50
+ @downloads.delete_if { |d| d.status == :finished || d.status == :failed }
51
+ end
52
+
53
+ class Download
54
+ attr_accessor :status
55
+ attr_reader :uri, :save_as, :callback, :remaining_bytes, :total_downloaded_bytes, :total_bytes,
56
+ :error_message, :started_at, :finished_at
57
+
58
+ def initialize(uri:, save_as:, callback: nil)
59
+ @uri = uri
60
+ @save_as = save_as
61
+ @callback = callback
62
+
63
+ @status = :pending
64
+
65
+ @remaining_bytes = 0.0
66
+ @total_downloaded_bytes = 0.0
67
+ @total_bytes = 0.0
68
+
69
+ @error_message = ""
70
+ end
71
+
72
+ def progress
73
+ v = 1.0 - (@remaining_bytes.to_f / total_bytes)
74
+ return 0.0 if v.nan?
75
+
76
+ v
77
+ end
78
+
79
+ def download
80
+ @status = :downloading
81
+ @started_at = Time.now # TODO: monotonic time
82
+
83
+ io = File.open(@save_as, "w")
84
+ streamer = lambda do |chunk, remaining_bytes, total_bytes|
85
+ io.write(chunk)
86
+
87
+ @remaining_bytes = remaining_bytes.to_f
88
+ @total_downloaded_bytes += chunk.size
89
+ @total_bytes = total_bytes.to_f
90
+ end
91
+
92
+ begin
93
+ response = Excon.get(
94
+ @uri.to_s,
95
+ middlewares: Excon.defaults[:middlewares] + [Excon::Middleware::RedirectFollower],
96
+ response_block: streamer
97
+ )
98
+
99
+ if response.status == 200
100
+ @status = :finished
101
+ @finished_at = Time.now # TODO: monotonic time
102
+ @callback.call(self) if @callback
103
+ else
104
+ @error_message = "Got a non 200 HTTP status of #{response.status}"
105
+ @status = :failed
106
+ @finished_at = Time.now # TODO: monotonic time
107
+ @callback.call(self) if @callback
108
+ end
109
+ rescue StandardError => e # TODO: cherrypick errors to cature
110
+ @status = :failed
111
+ @finished_at = Time.now # TODO: monotonic time
112
+ @error_message = e.message
113
+ @callback.call(self) if @callback
114
+ end
115
+ ensure
116
+ io.close if io
117
+ end
118
+ end
119
+ end
120
+ end
121
+ end