hytale 0.0.1 → 0.1.1

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 (56) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE.txt +21 -0
  3. data/README.md +1430 -15
  4. data/exe/hytale +497 -0
  5. data/lib/hytale/client/assets.rb +207 -0
  6. data/lib/hytale/client/block_type.rb +169 -0
  7. data/lib/hytale/client/config.rb +98 -0
  8. data/lib/hytale/client/cosmetics.rb +95 -0
  9. data/lib/hytale/client/item_type.rb +248 -0
  10. data/lib/hytale/client/launcher_log/launcher_log_entry.rb +58 -0
  11. data/lib/hytale/client/launcher_log/launcher_log_session.rb +57 -0
  12. data/lib/hytale/client/launcher_log.rb +99 -0
  13. data/lib/hytale/client/locale.rb +234 -0
  14. data/lib/hytale/client/map/block.rb +135 -0
  15. data/lib/hytale/client/map/chunk.rb +695 -0
  16. data/lib/hytale/client/map/marker.rb +50 -0
  17. data/lib/hytale/client/map/region.rb +278 -0
  18. data/lib/hytale/client/map/renderer.rb +435 -0
  19. data/lib/hytale/client/map.rb +271 -0
  20. data/lib/hytale/client/memories.rb +59 -0
  21. data/lib/hytale/client/npc_memory.rb +39 -0
  22. data/lib/hytale/client/permissions.rb +52 -0
  23. data/lib/hytale/client/player/entity_stats.rb +46 -0
  24. data/lib/hytale/client/player/inventory.rb +54 -0
  25. data/lib/hytale/client/player/item.rb +102 -0
  26. data/lib/hytale/client/player/item_storage.rb +40 -0
  27. data/lib/hytale/client/player/player_memory.rb +29 -0
  28. data/lib/hytale/client/player/position.rb +12 -0
  29. data/lib/hytale/client/player/rotation.rb +11 -0
  30. data/lib/hytale/client/player/vector3.rb +12 -0
  31. data/lib/hytale/client/player.rb +101 -0
  32. data/lib/hytale/client/player_skin.rb +179 -0
  33. data/lib/hytale/client/prefab/palette_entry.rb +49 -0
  34. data/lib/hytale/client/prefab.rb +184 -0
  35. data/lib/hytale/client/process.rb +57 -0
  36. data/lib/hytale/client/save/backup.rb +43 -0
  37. data/lib/hytale/client/save/server_log.rb +42 -0
  38. data/lib/hytale/client/save.rb +157 -0
  39. data/lib/hytale/client/settings/audio_settings.rb +30 -0
  40. data/lib/hytale/client/settings/builder_tools_settings.rb +27 -0
  41. data/lib/hytale/client/settings/gameplay_settings.rb +23 -0
  42. data/lib/hytale/client/settings/input_bindings.rb +25 -0
  43. data/lib/hytale/client/settings/mouse_settings.rb +23 -0
  44. data/lib/hytale/client/settings/rendering_settings.rb +30 -0
  45. data/lib/hytale/client/settings.rb +74 -0
  46. data/lib/hytale/client/world/client_effects.rb +24 -0
  47. data/lib/hytale/client/world/death_settings.rb +22 -0
  48. data/lib/hytale/client/world.rb +88 -0
  49. data/lib/hytale/client/zone/region.rb +52 -0
  50. data/lib/hytale/client/zone.rb +68 -0
  51. data/lib/hytale/client.rb +142 -0
  52. data/lib/hytale/server/process.rb +11 -0
  53. data/lib/hytale/server.rb +6 -0
  54. data/lib/hytale/version.rb +1 -1
  55. data/lib/hytale.rb +37 -2
  56. metadata +119 -10
data/exe/hytale ADDED
@@ -0,0 +1,497 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "hytale"
5
+
6
+ class CLI
7
+ COMMANDS = {
8
+ "info" => "Show Hytale installation info",
9
+ "settings" => "Show game settings",
10
+ "saves" => "List all saves",
11
+ "save" => "Show details for a specific save",
12
+ "player" => "Show player details",
13
+ "map" => "Show map of explored regions",
14
+ "prefabs" => "List prefabs by category",
15
+ "log" => "Show launcher log summary",
16
+ "help" => "Show this help message",
17
+ }.freeze
18
+
19
+ def initialize(args)
20
+ @args = args
21
+ @command = args.first || "info"
22
+ end
23
+
24
+ def run
25
+ unless Hytale.client.installed?
26
+ puts "Hytale is not installed on this system."
27
+ puts "Expected data at: #{Hytale::Client::Config.data_path}"
28
+ exit 1
29
+ end
30
+
31
+ case @command
32
+ when "info" then info
33
+ when "settings" then settings
34
+ when "saves" then saves
35
+ when "save" then save
36
+ when "player" then player
37
+ when "map" then map
38
+ when "prefabs" then prefabs
39
+ when "log" then log
40
+ when "help", "-h", "--help" then help
41
+ else
42
+ puts "Unknown command: #{@command}"
43
+ puts
44
+ help
45
+ exit 1
46
+ end
47
+ end
48
+
49
+ private
50
+
51
+ def info
52
+ puts "Hytale for Ruby v#{Hytale::VERSION}"
53
+ puts
54
+ puts "Installation"
55
+ puts " Data path: #{Hytale::Client::Config.data_path}"
56
+ puts " Installed: #{Hytale.client.installed?}"
57
+ puts " Running: #{Hytale.client.running?}"
58
+ puts
59
+
60
+ log = Hytale.launcher_log
61
+ puts "Launcher"
62
+ puts " Version: #{log.current_version}"
63
+ puts " Channel: #{log.current_channel}"
64
+ puts " Profile: #{log.current_profile_uuid}"
65
+ puts " Sessions: #{log.sessions.count}"
66
+ puts " Launches: #{log.game_launches.count}"
67
+ puts
68
+
69
+ settings = Hytale.settings
70
+ puts "Settings"
71
+ puts " Window: #{settings.window_size.join("x")}"
72
+ puts " FPS Limit: #{settings.fps_limit}"
73
+ puts " FOV: #{settings.field_of_view}"
74
+ puts " Fullscreen: #{settings.fullscreen?}"
75
+ puts
76
+
77
+ saves = Hytale.saves
78
+ puts "Saves (#{saves.count})"
79
+ saves.each do |save|
80
+ world = save.world
81
+ puts " #{save.name}"
82
+ puts " Mode: #{world.game_mode}, Players: #{save.players.count}, Backups: #{save.backups.count}"
83
+ end
84
+ end
85
+
86
+ def settings
87
+ s = Hytale.settings
88
+
89
+ puts "Display"
90
+ puts " Window: #{s.window_size.join("x")}"
91
+ puts " Fullscreen: #{s.fullscreen?}"
92
+ puts " VSync: #{s.vsync?}"
93
+ puts " FPS Limit: #{s.fps_limit}"
94
+ puts " FOV: #{s.field_of_view}"
95
+ puts
96
+
97
+ puts "Rendering"
98
+ puts " View Distance: #{s.rendering.view_distance}"
99
+ puts " Anti-Aliasing: #{s.rendering.anti_aliasing}"
100
+ puts " Shadows: #{s.rendering.shadows}"
101
+ puts " Bloom: #{s.rendering.bloom}"
102
+ puts " Depth of Field: #{s.rendering.depth_of_field}"
103
+ puts
104
+
105
+ puts "Audio"
106
+ puts " Master: #{(s.audio.master_volume * 100).round}%"
107
+ puts " Music: #{(s.audio.music_volume * 100).round}%"
108
+ puts " SFX: #{(s.audio.sfx_volume * 100).round}%"
109
+ puts " UI: #{(s.audio.ui_volume * 100).round}%"
110
+ puts
111
+
112
+ puts "Gameplay"
113
+ puts " Arachnophobia Mode: #{s.gameplay.arachnophobia_mode?}"
114
+ puts " Auto Jump: #{s.gameplay.auto_jump_obstacle?}"
115
+ puts " Camera Flying: #{s.gameplay.camera_based_flying?}"
116
+ puts
117
+
118
+ puts "Mouse"
119
+ puts " Sensitivity: #{s.mouse_settings.x_speed} x #{s.mouse_settings.y_speed}"
120
+ puts " Inverted: #{s.mouse_settings.inverted?}"
121
+ end
122
+
123
+ def saves
124
+ saves = Hytale.saves
125
+
126
+ if saves.empty?
127
+ puts "No saves found."
128
+ return
129
+ end
130
+
131
+ puts "Saves (#{saves.count})"
132
+ puts
133
+
134
+ saves.each do |save|
135
+ world = save.world
136
+ puts save.name
137
+ puts " Seed: #{world.seed}"
138
+ puts " Mode: #{world.game_mode}"
139
+ puts " PvP: #{world.pvp_enabled?}"
140
+ puts " Day/Night: #{world.daytime_duration}s / #{world.nighttime_duration}s"
141
+ puts " Players: #{save.players.count}"
142
+ puts " Memories: #{save.memories.count}"
143
+ puts " Backups: #{save.backups.count}"
144
+ puts " Mods: #{save.mods.any? ? save.mods.join(", ") : "none"}"
145
+ puts
146
+ end
147
+ end
148
+
149
+ def save
150
+ name = @args[1]
151
+
152
+ unless name
153
+ puts "Usage: hytale save <name>"
154
+ puts
155
+ puts "Available saves:"
156
+ Hytale.saves.each { |s| puts " #{s.name}" }
157
+ return
158
+ end
159
+
160
+ begin
161
+ save = Hytale.client.save(name)
162
+ rescue Hytale::NotFoundError
163
+ puts "Save not found: #{name}"
164
+ puts
165
+ puts "Available saves:"
166
+ Hytale.saves.each { |s| puts " #{s.name}" }
167
+ return
168
+ end
169
+
170
+ world = save.world
171
+
172
+ puts "Save: #{save.name}"
173
+ puts
174
+ puts "World"
175
+ puts " Display Name: #{world.display_name}"
176
+ puts " Seed: #{world.seed}"
177
+ puts " Game Mode: #{world.game_mode}"
178
+ puts " PvP: #{world.pvp_enabled?}"
179
+ puts " Fall Damage: #{world.fall_damage_enabled?}"
180
+ puts " Day Duration: #{world.daytime_duration}s"
181
+ puts " Night Duration: #{world.nighttime_duration}s"
182
+ puts
183
+
184
+ puts "Death Settings"
185
+ puts " Items Loss: #{world.death_settings.items_loss_percentage}%"
186
+ puts " Durability Loss: #{world.death_settings.durability_loss_percentage}%"
187
+ puts
188
+
189
+ puts "Players (#{save.players.count})"
190
+ save.players.each do |p|
191
+ puts " #{p.name}"
192
+ puts " Position: #{p.position}"
193
+ puts " Health: #{p.stats.health}"
194
+ puts " Stamina: #{p.stats.stamina&.round(1)}"
195
+ puts " Mode: #{p.game_mode}"
196
+ puts " Zones: #{p.discovered_zones.count} discovered"
197
+ end
198
+ puts
199
+
200
+ memories = save.memories
201
+ puts "Memories (#{memories.count} creatures discovered)"
202
+ if memories.any?
203
+ puts " Locations: #{memories.locations.join(", ")}"
204
+ puts " Recent:"
205
+ memories.all.sort_by { |m| m.captured_at || Time.at(0) }.last(5).each do |m|
206
+ puts " #{m.friendly_name} at #{m.location}"
207
+ end
208
+ end
209
+ puts
210
+
211
+ puts "Backups (#{save.backups.count})"
212
+ save.backups.first(5).each do |b|
213
+ puts " #{b.filename} (#{b.size_mb} MB)"
214
+ end
215
+ puts
216
+
217
+ return unless save.mods.any?
218
+
219
+ puts "Mods (#{save.mods.count})"
220
+ save.mods.each { |m| puts " #{m}" }
221
+ end
222
+
223
+ def player
224
+ save_name = @args[1]
225
+ player_name = @args[2]
226
+
227
+ unless save_name
228
+ puts "Usage: hytale player <save> [player_name]"
229
+ puts
230
+ puts "Available saves:"
231
+ Hytale.saves.each { |s| puts " #{s.name}" }
232
+ return
233
+ end
234
+
235
+ begin
236
+ save = Hytale.client.save(save_name)
237
+ rescue Hytale::NotFoundError
238
+ puts "Save not found: #{save_name}"
239
+ return
240
+ end
241
+
242
+ players = save.players
243
+
244
+ if players.empty?
245
+ puts "No players found in save: #{save_name}"
246
+ return
247
+ end
248
+
249
+ player = if player_name
250
+ players.find { |p| p.name.downcase == player_name.downcase }
251
+ else
252
+ players.first
253
+ end
254
+
255
+ unless player
256
+ puts "Player not found: #{player_name}"
257
+ puts
258
+ puts "Available players:"
259
+ players.each { |p| puts " #{p.name}" }
260
+ return
261
+ end
262
+
263
+ puts "Player: #{player.name}"
264
+ puts " UUID: #{player.uuid}"
265
+ puts
266
+ puts "Position"
267
+ puts " Location: #{player.position}"
268
+ puts " Rotation: #{player.rotation}"
269
+ puts " World: #{player.current_world}"
270
+ puts
271
+
272
+ puts "Stats"
273
+ puts " Health: #{player.stats.health} / #{player.stats.max_health}"
274
+ puts " Stamina: #{player.stats.stamina&.round(1)}"
275
+ puts " Oxygen: #{player.stats.oxygen}"
276
+ puts " Mana: #{player.stats.mana}"
277
+ puts
278
+
279
+ puts "Progress"
280
+ puts " Game Mode: #{player.game_mode}"
281
+ puts " Zones: #{player.discovered_zones.count} discovered"
282
+ player.discovered_zones.each { |z| puts " #{z}" }
283
+ puts
284
+
285
+ puts "Hotbar"
286
+ player.inventory.hotbar.items.each do |item|
287
+ puts " [#{item.slot}] #{item}"
288
+ end
289
+ puts
290
+
291
+ puts "Armor"
292
+ armor_slots = ["Head", "Chest", "Hands", "Legs"]
293
+ player.inventory.armor.items.each do |item|
294
+ slot_name = armor_slots[item.slot] || item.slot
295
+ puts " #{slot_name}: #{item}"
296
+ end
297
+ puts
298
+
299
+ puts "Inventory (#{player.inventory.storage.items.count} items)"
300
+ player.inventory.storage.items.first(10).each do |item|
301
+ puts " #{item}"
302
+ end
303
+ puts " ... and #{player.inventory.storage.items.count - 10} more" if player.inventory.storage.items.count > 10
304
+ puts
305
+
306
+ puts "Memories (#{player.memories.count})"
307
+ player.memories.last(5).each do |m|
308
+ puts " #{m.npc_role} at #{m.location}"
309
+ end
310
+ end
311
+
312
+ def map
313
+ save_name = @args[1]
314
+
315
+ unless save_name
316
+ puts "Usage: hytale map <save>"
317
+ puts
318
+ puts "Available saves:"
319
+ Hytale.saves.each { |s| puts " #{s.name}" }
320
+ return
321
+ end
322
+
323
+ begin
324
+ save = Hytale.client.save(save_name)
325
+ rescue Hytale::NotFoundError
326
+ puts "Save not found: #{save_name}"
327
+ return
328
+ end
329
+
330
+ map = save.map
331
+ players = save.players
332
+
333
+ puts "Map: #{save.name}"
334
+ puts
335
+ puts "Coverage"
336
+ puts " Regions: #{map.regions.count}"
337
+ puts " Size: #{map.total_size_mb} MB"
338
+
339
+ if map.bounds
340
+ b = map.bounds
341
+ puts " Bounds: X: #{b[:min_x]} to #{b[:max_x]}, Z: #{b[:min_z]} to #{b[:max_z]}"
342
+ puts " Area: #{b[:width]} x #{b[:height]} regions"
343
+ end
344
+ puts
345
+
346
+ if map.markers.any?
347
+ puts "Markers (#{map.markers.count})"
348
+ map.markers.each do |marker|
349
+ puts " #{marker.name} at (#{marker.x}, #{marker.y}, #{marker.z})"
350
+ end
351
+ puts
352
+ end
353
+
354
+ puts "Regions"
355
+ map.regions.sort_by { |r| [r.x, r.z] }.each do |region|
356
+ puts " (#{region.x}, #{region.z}) - #{region.size_mb} MB (#{region.modified_at.strftime("%Y-%m-%d %H:%M")})"
357
+ end
358
+ puts
359
+
360
+ puts "Players"
361
+ players.each do |p|
362
+ pos = p.position
363
+ puts " #{p.name}: (#{pos.x.round}, #{pos.y.round}, #{pos.z.round})"
364
+ end
365
+ puts
366
+
367
+ puts "Map (explored regions)"
368
+ puts map.to_ascii(players: players)
369
+ end
370
+
371
+ def prefabs
372
+ category = @args[1]
373
+
374
+ categories = Hytale.client.prefab_categories
375
+
376
+ if categories.empty?
377
+ puts "No prefabs found."
378
+ puts "Expected at: #{Hytale::Client::Config.prefab_cache_path}"
379
+ return
380
+ end
381
+
382
+ if category
383
+ prefabs = Hytale.client.prefabs_in_category(category)
384
+
385
+ if prefabs.empty?
386
+ puts "No prefabs found in category: #{category}"
387
+ puts
388
+ puts "Available categories:"
389
+ categories.each { |c| puts " #{c}" }
390
+ return
391
+ end
392
+
393
+ puts "Prefabs in #{category} (#{prefabs.count})"
394
+ puts
395
+
396
+ by_subcategory = prefabs.group_by(&:subcategory)
397
+
398
+ by_subcategory.each do |subcat, subprefabs|
399
+ if subcat
400
+ puts " #{subcat} (#{subprefabs.count})"
401
+ subprefabs.first(5).each do |p|
402
+ puts " #{p.name} - #{p.palette.size} block types"
403
+ end
404
+ puts " ... and #{subprefabs.count - 5} more" if subprefabs.count > 5
405
+ else
406
+ subprefabs.first(10).each do |p|
407
+ puts " #{p.name} - #{p.palette.size} block types"
408
+ end
409
+ puts " ... and #{subprefabs.count - 10} more" if subprefabs.count > 10
410
+ end
411
+ puts
412
+ end
413
+ else
414
+ total = Hytale.client.prefabs.count
415
+
416
+ puts "Prefabs (#{total} total)"
417
+ puts
418
+
419
+ categories.each do |cat|
420
+ count = Hytale.client.prefabs_in_category(cat).count
421
+ puts " #{cat.ljust(20)} #{count}"
422
+ end
423
+
424
+ puts
425
+ puts "Usage: hytale prefabs <category>"
426
+ puts
427
+ puts "Example:"
428
+ puts " hytale prefabs Trees"
429
+ end
430
+ end
431
+
432
+ def log
433
+ log = Hytale.launcher_log
434
+
435
+ puts "Launcher Log"
436
+ puts " Path: #{log.path}"
437
+ puts " Version: #{log.current_version}"
438
+ puts " Channel: #{log.current_channel}"
439
+ puts " Profile: #{log.current_profile_uuid}"
440
+ puts
441
+
442
+ puts "Summary"
443
+ puts " Total Entries: #{log.entries.count}"
444
+ puts " Sessions: #{log.sessions.count}"
445
+ puts " Game Launches: #{log.game_launches.count}"
446
+ puts " Updates: #{log.updates.count}"
447
+ puts " Errors: #{log.errors.count}"
448
+ puts " Warnings: #{log.warnings.count}"
449
+ puts
450
+
451
+ puts "Recent Sessions"
452
+
453
+ log.sessions.last(5).each do |session|
454
+ status = session.game_launched? ? "launched" : "no launch"
455
+ errors = session.errors.any? ? ", #{session.errors.count} errors" : ""
456
+
457
+ puts " #{session.started_at&.strftime("%Y-%m-%d %H:%M")} - v#{session.version} (#{status}#{errors})"
458
+ end
459
+
460
+ puts
461
+
462
+ if log.errors.any?
463
+ puts "Recent Errors"
464
+ log.errors.last(5).each do |e|
465
+ puts " [#{e.timestamp&.strftime("%Y-%m-%d %H:%M")}] #{e.message}"
466
+ end
467
+ puts
468
+ end
469
+
470
+ puts "Recent Game Launches"
471
+ log.game_launches.last(5).each do |e|
472
+ puts " #{e.timestamp&.strftime("%Y-%m-%d %H:%M:%S")}"
473
+ end
474
+ end
475
+
476
+ def help
477
+ puts "Hytale CLI v#{Hytale::VERSION}"
478
+ puts
479
+ puts "Usage: hytale <command> [options]"
480
+ puts
481
+ puts "Commands:"
482
+ COMMANDS.each do |cmd, desc|
483
+ puts " #{cmd.ljust(12)} #{desc}"
484
+ end
485
+ puts
486
+ puts "Examples:"
487
+ puts " hytale info"
488
+ puts " hytale saves"
489
+ puts ' hytale save "New World"'
490
+ puts ' hytale player "New World" marcoroth'
491
+ puts ' hytale map "New World"'
492
+ puts " hytale prefabs Trees"
493
+ puts " hytale log"
494
+ end
495
+ end
496
+
497
+ CLI.new(ARGV).run
@@ -0,0 +1,207 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hytale
4
+ module Client
5
+ class Assets
6
+ BLOCK_TEXTURES_PATH = "Common/BlockTextures"
7
+ ITEM_ICONS_PATH = "Common/Icons/ItemsGenerated"
8
+
9
+ class << self
10
+ def cache_path
11
+ Config.assets_cache_path
12
+ end
13
+
14
+ def zip_path
15
+ Config.assets_path
16
+ end
17
+
18
+ def ensure_extracted!
19
+ return if @extracted
20
+ return unless zip_path
21
+
22
+ extract_all unless File.directory?(cache_path) && count.positive?
23
+
24
+ @extracted = true
25
+ end
26
+
27
+ def cached?(path)
28
+ ensure_extracted!
29
+
30
+ File.exist?(cached_path(path))
31
+ end
32
+
33
+ def cached_path(path)
34
+ File.expand_path(path, cache_path)
35
+ end
36
+
37
+ def read(path)
38
+ full_path = cached_path(path)
39
+ return File.binread(full_path) if File.exist?(full_path)
40
+
41
+ nil
42
+ end
43
+
44
+ def extract_all
45
+ return 0 unless zip_path
46
+
47
+ require_zip!
48
+
49
+ count = 0
50
+
51
+ Zip::File.open(zip_path) do |zip|
52
+ zip.entries.each do |entry|
53
+ next if entry.directory?
54
+
55
+ output_path = cached_path(entry.name)
56
+
57
+ next if File.exist?(output_path)
58
+
59
+ FileUtils.mkdir_p(File.dirname(output_path))
60
+ File.binwrite(output_path, entry.get_input_stream.read)
61
+
62
+ count += 1
63
+ end
64
+ end
65
+
66
+ count
67
+ end
68
+
69
+ def extract(path)
70
+ return false unless zip_path
71
+
72
+ require_zip!
73
+
74
+ output_path = cached_path(path)
75
+ return true if File.exist?(output_path)
76
+
77
+ Zip::File.open(zip_path) do |zip|
78
+ entry = zip.find_entry(path)
79
+ return false unless entry
80
+
81
+ FileUtils.mkdir_p(File.dirname(output_path))
82
+ File.binwrite(output_path, entry.get_input_stream.read)
83
+ end
84
+
85
+ true
86
+ rescue Zip::Error
87
+ false
88
+ end
89
+
90
+ def extract_directory(dir_path)
91
+ return 0 unless zip_path
92
+
93
+ require_zip!
94
+
95
+ count = 0
96
+ prefix = dir_path.end_with?("/") ? dir_path : "#{dir_path}/"
97
+
98
+ Zip::File.open(zip_path) do |zip|
99
+ zip.entries.each do |entry|
100
+ next if entry.directory?
101
+ next unless entry.name.start_with?(prefix)
102
+
103
+ output_path = cached_path(entry.name)
104
+ next if File.exist?(output_path)
105
+
106
+ FileUtils.mkdir_p(File.dirname(output_path))
107
+ File.binwrite(output_path, entry.get_input_stream.read)
108
+
109
+ count += 1
110
+ end
111
+ end
112
+
113
+ count
114
+ end
115
+
116
+ def list(dir_path = nil)
117
+ return [] unless zip_path
118
+
119
+ require_zip!
120
+
121
+ prefix = if dir_path
122
+ dir_path.end_with?("/") ? dir_path : "#{dir_path}/"
123
+ end
124
+
125
+ Zip::File.open(zip_path) do |zip|
126
+ entries = zip.entries.reject(&:directory?)
127
+ entries = entries.select { |e| e.name.start_with?(prefix) } if prefix
128
+
129
+ entries.map(&:name).sort
130
+ end
131
+ rescue Zip::Error
132
+ []
133
+ end
134
+
135
+ def directories
136
+ return [] unless zip_path
137
+
138
+ require_zip!
139
+
140
+ Zip::File.open(zip_path) do |zip|
141
+ zip.entries
142
+ .map { |e| e.name.split("/").first(2).join("/") }
143
+ .uniq
144
+ .reject { |d| d.include?(".") }
145
+ .sort
146
+ end
147
+ rescue Zip::Error
148
+ []
149
+ end
150
+
151
+ def clear!
152
+ FileUtils.rm_rf(cache_path)
153
+ end
154
+
155
+ def count
156
+ return 0 unless File.directory?(cache_path)
157
+
158
+ Dir.glob(File.join(cache_path, "**", "*")).count { |f| File.file?(f) }
159
+ end
160
+
161
+ def block_textures
162
+ list(BLOCK_TEXTURES_PATH)
163
+ .reject { |p| p.include?("/_") }
164
+ .map { |p| File.basename(p, ".png") }
165
+ end
166
+
167
+ def extract_block_textures
168
+ extract_directory(BLOCK_TEXTURES_PATH)
169
+ end
170
+
171
+ def block_texture_path(name)
172
+ name = "#{name}.png" unless name.end_with?(".png")
173
+
174
+ cached_path("#{BLOCK_TEXTURES_PATH}/#{name}")
175
+ end
176
+
177
+ def item_icons
178
+ list(ITEM_ICONS_PATH).map { |p| File.basename(p, ".png") }
179
+ end
180
+
181
+ def extract_item_icons
182
+ extract_directory(ITEM_ICONS_PATH)
183
+ end
184
+
185
+ def item_icon_path(name)
186
+ name = "#{name}.png" unless name.end_with?(".png")
187
+
188
+ cached_path("#{ITEM_ICONS_PATH}/#{name}")
189
+ end
190
+
191
+ def item_icon_exists?(name)
192
+ ensure_extracted!
193
+
194
+ File.exist?(item_icon_path(name))
195
+ end
196
+
197
+ private
198
+
199
+ def require_zip!
200
+ require "zip"
201
+ rescue LoadError
202
+ raise Error, "rubyzip gem required for asset extraction: gem install rubyzip"
203
+ end
204
+ end
205
+ end
206
+ end
207
+ end