cyberarm_engine 0.13.0 → 0.17.0

Sign up to get free protection for your applications and to get access to all the features.
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