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.
- checksums.yaml +4 -4
- data/LICENSE.txt +21 -0
- data/README.md +1430 -15
- data/exe/hytale +497 -0
- data/lib/hytale/client/assets.rb +207 -0
- data/lib/hytale/client/block_type.rb +169 -0
- data/lib/hytale/client/config.rb +98 -0
- data/lib/hytale/client/cosmetics.rb +95 -0
- data/lib/hytale/client/item_type.rb +248 -0
- data/lib/hytale/client/launcher_log/launcher_log_entry.rb +58 -0
- data/lib/hytale/client/launcher_log/launcher_log_session.rb +57 -0
- data/lib/hytale/client/launcher_log.rb +99 -0
- data/lib/hytale/client/locale.rb +234 -0
- data/lib/hytale/client/map/block.rb +135 -0
- data/lib/hytale/client/map/chunk.rb +695 -0
- data/lib/hytale/client/map/marker.rb +50 -0
- data/lib/hytale/client/map/region.rb +278 -0
- data/lib/hytale/client/map/renderer.rb +435 -0
- data/lib/hytale/client/map.rb +271 -0
- data/lib/hytale/client/memories.rb +59 -0
- data/lib/hytale/client/npc_memory.rb +39 -0
- data/lib/hytale/client/permissions.rb +52 -0
- data/lib/hytale/client/player/entity_stats.rb +46 -0
- data/lib/hytale/client/player/inventory.rb +54 -0
- data/lib/hytale/client/player/item.rb +102 -0
- data/lib/hytale/client/player/item_storage.rb +40 -0
- data/lib/hytale/client/player/player_memory.rb +29 -0
- data/lib/hytale/client/player/position.rb +12 -0
- data/lib/hytale/client/player/rotation.rb +11 -0
- data/lib/hytale/client/player/vector3.rb +12 -0
- data/lib/hytale/client/player.rb +101 -0
- data/lib/hytale/client/player_skin.rb +179 -0
- data/lib/hytale/client/prefab/palette_entry.rb +49 -0
- data/lib/hytale/client/prefab.rb +184 -0
- data/lib/hytale/client/process.rb +57 -0
- data/lib/hytale/client/save/backup.rb +43 -0
- data/lib/hytale/client/save/server_log.rb +42 -0
- data/lib/hytale/client/save.rb +157 -0
- data/lib/hytale/client/settings/audio_settings.rb +30 -0
- data/lib/hytale/client/settings/builder_tools_settings.rb +27 -0
- data/lib/hytale/client/settings/gameplay_settings.rb +23 -0
- data/lib/hytale/client/settings/input_bindings.rb +25 -0
- data/lib/hytale/client/settings/mouse_settings.rb +23 -0
- data/lib/hytale/client/settings/rendering_settings.rb +30 -0
- data/lib/hytale/client/settings.rb +74 -0
- data/lib/hytale/client/world/client_effects.rb +24 -0
- data/lib/hytale/client/world/death_settings.rb +22 -0
- data/lib/hytale/client/world.rb +88 -0
- data/lib/hytale/client/zone/region.rb +52 -0
- data/lib/hytale/client/zone.rb +68 -0
- data/lib/hytale/client.rb +142 -0
- data/lib/hytale/server/process.rb +11 -0
- data/lib/hytale/server.rb +6 -0
- data/lib/hytale/version.rb +1 -1
- data/lib/hytale.rb +37 -2
- 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
|