hytale 0.0.1 → 0.1.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 (54) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE.txt +21 -0
  3. data/README.md +1315 -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 +55 -0
  12. data/lib/hytale/client/launcher_log.rb +94 -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 +693 -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 +437 -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 +46 -0
  25. data/lib/hytale/client/player/item.rb +102 -0
  26. data/lib/hytale/client/player/item_storage.rb +32 -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 +99 -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 +81 -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.rb +142 -0
  50. data/lib/hytale/server/process.rb +8 -0
  51. data/lib/hytale/server.rb +6 -0
  52. data/lib/hytale/version.rb +1 -1
  53. data/lib/hytale.rb +37 -2
  54. metadata +117 -10
data/README.md CHANGED
@@ -1,39 +1,1339 @@
1
- # Hytale
1
+ <div align="center">
2
+ <h1>Hytale Ruby</h1>
3
+ <h4>Ruby gem for reading Hytale game data.</h4>
2
4
 
3
- TODO: Delete this and the text below, and describe your gem
5
+ <p>
6
+ <a href="https://rubygems.org/gems/hytale"><img alt="Gem Version" src="https://img.shields.io/gem/v/hytale"></a>
7
+ <a href="https://github.com/marcoroth/hytale-ruby/blob/main/LICENSE.txt"><img alt="License" src="https://img.shields.io/github/license/marcoroth/hytale"></a>
8
+ </p>
4
9
 
5
- Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/hytale`. To experiment with that code, run `bin/console` for an interactive prompt.
10
+ <p>Read and parse Hytale game data including settings, saves, players, and launcher logs.<br/>Cross-platform support for macOS, Windows, and Linux.</p>
11
+ </div>
6
12
 
7
13
  ## Installation
8
14
 
9
- TODO: Replace `UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG` with your gem name right after releasing it to RubyGems.org. Please do not do it earlier due to security reasons. Alternatively, replace this section with instructions to install your gem from git if you don't plan to release to RubyGems.org.
15
+ **Add to your Gemfile:**
10
16
 
11
- Install the gem and add to the application's Gemfile by executing:
17
+ ```ruby
18
+ gem "hytale"
19
+ ```
20
+
21
+ **Or install directly:**
22
+
23
+ ```bash
24
+ gem install hytale
25
+ ```
26
+
27
+ ## CLI
28
+
29
+ The gem includes a `hytale` command for quick access to game data.
30
+
31
+ **Show installation info:**
12
32
 
13
33
  ```bash
14
- bundle add UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG
34
+ hytale info
15
35
  ```
16
36
 
17
- If bundler is not being used to manage dependencies, install the gem by executing:
37
+ **List all saves:**
18
38
 
19
39
  ```bash
20
- gem install UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG
40
+ hytale saves
21
41
  ```
22
42
 
43
+ **Show save details:**
44
+
45
+ ```bash
46
+ hytale save "New World"
47
+ ```
48
+
49
+ **Show player details:**
50
+
51
+ ```bash
52
+ hytale player "New World" marcoroth
53
+ ```
54
+
55
+ **Show game settings:**
56
+
57
+ ```bash
58
+ hytale settings
59
+ ```
60
+
61
+ **Show launcher log:**
62
+
63
+ ```bash
64
+ hytale log
65
+ ```
66
+
67
+ **List prefabs:**
68
+
69
+ ```bash
70
+ hytale prefabs
71
+ hytale prefabs Trees
72
+ ```
73
+
74
+ **Available commands:**
75
+
76
+ | Command | Description |
77
+ |---------|-------------|
78
+ | `info` | Show Hytale installation info |
79
+ | `settings` | Show game settings |
80
+ | `saves` | List all saves |
81
+ | `save <name>` | Show details for a specific save |
82
+ | `player <save> [name]` | Show player details |
83
+ | `map <save>` | Show map of explored regions |
84
+ | `prefabs [category]` | List prefabs by category |
85
+ | `log` | Show launcher log summary |
86
+ | `help` | Show help message |
87
+
23
88
  ## Usage
24
89
 
25
- TODO: Write usage instructions here
90
+ ### Quick Start
26
91
 
27
- ## Development
92
+ ```ruby
93
+ require "hytale"
94
+ ```
95
+
96
+ **Check if Hytale is installed:**
97
+
98
+ ```ruby
99
+ Hytale.client.installed? # => true
100
+ ```
101
+
102
+ **Read game settings:**
103
+
104
+ ```ruby
105
+ settings = Hytale.settings
106
+ settings.window_size # => [1280, 720]
107
+ settings.field_of_view # => 75
108
+ ```
109
+
110
+ **List all saves:**
111
+
112
+ ```ruby
113
+ Hytale.saves.each do |save|
114
+ puts "#{save.name}: #{save.world.game_mode}"
115
+ end
116
+ ```
117
+
118
+ ### Object Relationships
119
+
120
+ Understanding how the main objects relate to each other:
121
+
122
+ ```
123
+ Save (a saved game folder)
124
+ ├── World (configuration for a dimension)
125
+ │ └── Map (terrain data)
126
+ │ ├── Region (32x32 chunk file)
127
+ │ │ └── Chunk (16x16 block column)
128
+ │ └── Markers, Time, etc.
129
+ ├── Players
130
+ ├── Memories
131
+ └── Permissions, Bans, etc.
132
+ ```
133
+
134
+ | Object | Description |
135
+ |--------|-------------|
136
+ | **Save** | A saved game folder (e.g., "New World 1"). Contains worlds, players, permissions. |
137
+ | **World** | A dimension's configuration (`config.json`). Settings like seed, game mode, spawn point. |
138
+ | **Map** | The actual terrain data for a world. Contains regions with chunk data. |
139
+ | **Region** | A 32x32 chunk area stored in a `.region.bin` file. |
140
+ | **Chunk** | A 16x16 column of blocks. The smallest unit of terrain. |
141
+
142
+ **Navigating between objects:**
143
+
144
+ ```ruby
145
+ save = Hytale.saves.first
146
+ ```
147
+
148
+ **Save -> Worlds**
149
+ ```ruby
150
+ save.world_names # => ["default", "flat_world", ...]
151
+ save.worlds # => [World, World, ...]
152
+ ```
153
+
154
+ **Save -> World (specific)**
155
+ ```ruby
156
+ world = save.world("flat_world")
157
+ world.display_name # => "Flat"
158
+ world.game_mode # => "Creative"
159
+ ```
160
+
161
+ **World -> Map**
162
+ ```ruby
163
+ map = world.map
164
+ map.regions.count # => 4
165
+ ```
166
+
167
+ **Save -> Map (shortcut)**
168
+ ```ruby
169
+ map = save.map("flat_world")
170
+ ```
171
+
172
+ **Save -> All Maps**
173
+ ```ruby
174
+ save.maps.each do |map|
175
+ puts "#{map.world_name}: #{map.regions.count} regions"
176
+ end
177
+ ```
178
+
179
+ **Map -> Regions -> Chunks**
180
+ ```ruby
181
+ region = map.regions.first
182
+
183
+ region.each_chunk do |chunk|
184
+ puts "#{chunk.local_x}, #{chunk.local_z}: #{chunk.block_types.join(', ')}"
185
+ end
186
+ ```
187
+
188
+ ### Reading Settings
189
+
190
+ Settings provides access to all game configuration:
191
+
192
+ **Display:**
193
+
194
+ ```ruby
195
+ settings = Hytale.settings
196
+
197
+ settings.fullscreen? # => false
198
+ settings.window_size # => [1280, 720]
199
+ settings.vsync? # => true
200
+ settings.fps_limit # => 118
201
+ settings.field_of_view # => 75
202
+ ```
203
+
204
+ **Rendering:**
205
+
206
+ ```ruby
207
+ settings.rendering.view_distance # => 384
208
+ settings.rendering.anti_aliasing # => 3
209
+ settings.rendering.shadows # => 2
210
+ ```
211
+
212
+ **Audio:**
213
+
214
+ ```ruby
215
+ settings.audio.master_volume # => 0.85
216
+ settings.audio.music_volume # => 0.85
217
+ ```
218
+
219
+ **Gameplay:**
220
+
221
+ ```ruby
222
+ settings.gameplay.arachnophobia_mode? # => false
223
+ ```
224
+
225
+ **Settings modules:**
226
+
227
+ | Module | Description |
228
+ |--------|-------------|
229
+ | `rendering` | Graphics settings (view distance, shadows, AA, bloom) |
230
+ | `audio` | Volume levels and output device |
231
+ | `mouse_settings` | Sensitivity and inversion |
232
+ | `gameplay` | Game behavior options |
233
+ | `builder_tools` | Creative mode tool settings |
234
+ | `input_bindings` | Key bindings |
235
+
236
+ ### Working with Saves
237
+
238
+ Access world saves and their contents:
239
+
240
+ **List all saves:**
241
+
242
+ ```ruby
243
+ saves = Hytale.saves
244
+ ```
245
+
246
+ **Find a specific save:**
247
+
248
+ ```ruby
249
+ save = Hytale.client.save("New World")
250
+ ```
251
+
252
+ **World configuration:**
253
+
254
+ ```ruby
255
+ world = save.world
256
+ world.display_name # => "New World"
257
+ world.seed # => 1768313554213
258
+ world.game_mode # => "Adventure"
259
+ world.pvp_enabled? # => false
260
+ world.daytime_duration # => 1728
261
+ world.nighttime_duration # => 1151
262
+ ```
263
+
264
+ **Death settings:**
265
+
266
+ ```ruby
267
+ world.death_settings.items_loss_percentage # => 50.0
268
+ world.death_settings.durability_loss_percentage # => 10.0
269
+ ```
270
+
271
+ **Save contents:**
272
+
273
+ | Method | Description |
274
+ |--------|-------------|
275
+ | `world(name)` | World configuration (seed, game mode, day/night cycle) |
276
+ | `worlds` | All World objects in the save |
277
+ | `world_names` | List of world directory names |
278
+ | `map(name)` | Map data for a specific world |
279
+ | `maps` | All Map objects for all worlds |
280
+ | `players` | All players in this save |
281
+ | `memories` | Discovered NPCs/creatures |
282
+ | `permissions` | Server permissions and groups |
283
+ | `backups` | Automatic backup files |
284
+ | `logs` | Server log files |
285
+ | `mods` | Installed mods |
286
+
287
+ ### Reading Player Data
288
+
289
+ Access player inventory, stats, and progress:
290
+
291
+ **Basic info:**
292
+
293
+ ```ruby
294
+ save = Hytale.client.save("New World")
295
+ player = save.players.first
296
+
297
+ player.name # => "marcoroth"
298
+ player.uuid # => "79816d74-0500-4dad-9767-06af86c17243"
299
+ player.position # => (590.75, 123.0, 374.2)
300
+ player.game_mode # => "Adventure"
301
+ player.discovered_zones # => ["Zone1_Spawn", "Zone1_Tier1", ...]
302
+ player.skin # => PlayerSkin object
303
+ player.avatar_preview_path # => "/path/to/CachedAvatarPreviews/uuid.png"
304
+ ```
305
+
306
+ **Stats:**
307
+
308
+ ```ruby
309
+ player.stats.health # => 96.0
310
+ player.stats.stamina # => 9.3
311
+ player.stats.oxygen # => 100.0
312
+ ```
313
+
314
+ **Inventory:**
315
+
316
+ ```ruby
317
+ player.inventory.hotbar.items.each do |item|
318
+ puts "#{item.name} - #{item.durability_percent}%"
319
+ end
320
+ ```
321
+
322
+ **Armor:**
323
+
324
+ ```ruby
325
+ player.inventory.armor.items.each do |item|
326
+ puts item.name
327
+ end
328
+ ```
329
+
330
+ **Player inventory slots:**
331
+
332
+ | Slot | Description |
333
+ |------|-------------|
334
+ | `hotbar` | 9-slot quick access bar |
335
+ | `storage` | Main inventory (36 slots) |
336
+ | `armor` | Head, chest, hands, legs |
337
+ | `utility` | Utility items (4 slots) |
338
+ | `tools` | Builder/editor tools |
339
+
340
+ **Item properties:**
341
+
342
+ ```ruby
343
+ item.id # => "Tool_Pickaxe_Copper"
344
+ item.name # => "Tool Pickaxe Copper"
345
+ item.quantity # => 1
346
+ item.durability # => 29.75
347
+ item.max_durability # => 200.0
348
+ item.durability_percent # => 14.9
349
+ item.damaged? # => true
350
+ ```
351
+
352
+ ### Memories (Discovered Creatures)
353
+
354
+ Track discovered NPCs and creatures:
355
+
356
+ **Count and list:**
357
+
358
+ ```ruby
359
+ memories = save.memories
360
+ memories.count # => 42
361
+ ```
362
+
363
+ **List all discovered roles:**
364
+
365
+ ```ruby
366
+ memories.roles
367
+ # => ["Bat", "Bear_Grizzly", "Bluebird", "Boar", ...]
368
+ ```
369
+
370
+ **List discovery locations:**
371
+
372
+ ```ruby
373
+ memories.locations
374
+ # => ["ForgottenTemple", "Zone1_Tier1", "Zone1_Tier2", ...]
375
+ ```
376
+
377
+ **Find specific creatures:**
378
+
379
+ ```ruby
380
+ memories.find_by_role("Wolf_Black")
381
+ # => Memory: Wolf Black found at Zone1_Tier1
382
+
383
+ memories.find_all_by_location("ForgottenTemple")
384
+ # => [Memory: Duck, Memory: Kweebec_Rootling, ...]
385
+ ```
386
+
387
+ **Iterate:**
388
+
389
+ ```ruby
390
+ memories.each do |memory|
391
+ puts "#{memory.friendly_name} at #{memory.location} (#{memory.captured_at})"
392
+ end
393
+ ```
394
+
395
+ ### Permissions
396
+
397
+ Read server permissions and groups:
398
+
399
+ **List groups:**
400
+
401
+ ```ruby
402
+ permissions = save.permissions
403
+
404
+ permissions.groups
405
+ # => {"Default" => [], "OP" => ["*"]}
406
+ ```
407
+
408
+ **Check user permissions:**
409
+
410
+ ```ruby
411
+ permissions.user_groups(uuid)
412
+ # => ["Adventure"]
413
+
414
+ permissions.op?(uuid)
415
+ # => false
416
+ ```
417
+
418
+ ### Map Data
419
+
420
+ Access explored regions and map markers:
421
+
422
+ **Get map for a save:**
423
+
424
+ ```ruby
425
+ map = save.map
426
+ ```
427
+
428
+ **Region coverage:**
429
+
430
+ ```ruby
431
+ map.regions.count # => 10
432
+ map.total_size_mb # => 120.56
433
+ map.bounds # => {min_x: -1, max_x: 2, min_z: -1, max_z: 1, ...}
434
+ ```
435
+
436
+ **Individual regions:**
437
+
438
+ ```ruby
439
+ map.regions.each do |region|
440
+ puts "#{region.x}, #{region.z}: #{region.size_mb} MB"
441
+ end
442
+ ```
443
+
444
+ **Region details:**
445
+
446
+ ```ruby
447
+ region = map.regions.first
448
+ region.header # => {version: 1, chunk_count: 1024, ...}
449
+ region.chunk_count # => 207 (non-empty chunks)
450
+ region.block_types # => ["Rock_Stone", "Soil_Grass", ...]
451
+ ```
452
+
453
+ **Block types across all regions:**
454
+
455
+ ```ruby
456
+ map.block_types # => ["Ore_Copper", "Plant_Bush", "Rock_Stone", ...]
457
+ ```
458
+
459
+ **Map markers (discovered locations):**
460
+
461
+ ```ruby
462
+ map.markers.each do |marker|
463
+ puts "#{marker.name} at #{marker.position}"
464
+ end
465
+ # => "Forgotten Temple Portal Enter at (832, 113, 367)"
466
+ ```
467
+
468
+ **ASCII map of explored regions:**
469
+
470
+ ```ruby
471
+ puts map.to_ascii(players: save.players)
472
+ ```
473
+
474
+ ```
475
+ -1 0 1 2
476
+ --------------
477
+ -1 | o O O |
478
+ 0 | O # M . |
479
+ 1 | o O o |
480
+ --------------
481
+
482
+ Legend: . = small, o = medium, O = large, # = huge, Letter = player
483
+ ```
484
+
485
+ ### Global Coordinates
486
+
487
+ Access regions, chunks, and blocks using world coordinates:
488
+
489
+ ```ruby
490
+ map = save.map
491
+ ```
492
+
493
+ **Get region containing world coordinates:**
494
+
495
+ ```ruby
496
+ region = map.region_at_world(100, 200)
497
+ ```
498
+
499
+ **Get chunk at world coordinates:**
500
+
501
+ ```ruby
502
+ chunk = map.chunk_at(100, 200)
503
+ ```
504
+
505
+ **Get block at world coordinates:**
506
+
507
+ ```ruby
508
+ block = map.block_at(100, 50, 200)
509
+ block.id # => "Rock_Stone"
510
+ block.world_position # => [100, 50, 200]
511
+ ```
512
+
513
+ **Coordinate conversion helpers:**
514
+
515
+ ```ruby
516
+ map.world_to_region_coords(100, 200) # => [0, 0]
517
+ map.world_to_region_coords(-100, -200) # => [-1, -1]
518
+ map.world_to_chunk_local_coords(100, 200) # => [6, 12]
519
+ map.world_to_block_local_coords(100, 200) # => [4, 8]
520
+ ```
521
+
522
+ **Coordinate system:**
523
+
524
+ | Unit | Size | Description |
525
+ |------|------|-------------|
526
+ | Block | 1 | Smallest unit |
527
+ | Chunk | 16×16 blocks | Vertical column of blocks |
528
+ | Region | 32×32 chunks (512×512 blocks) | Stored in `.region.bin` files |
529
+
530
+ Region 0 covers blocks 0..511, region -1 covers -512..-1, etc.
531
+
532
+ ### Map Rendering
533
+
534
+ Generate PNG images of maps using colors derived from block textures.
535
+
536
+ **Render modes:**
537
+
538
+ | Mode | Description | Speed |
539
+ |------|-------------|-------|
540
+ | Fast (`detailed: false`) | Colors entire chunk with dominant surface block | ~5s for 10 regions |
541
+ | Detailed (`detailed: true`) | Renders each block individually using `surface_at` | ~15s for 10 regions |
542
+
543
+ **Render a map to PNG:**
544
+
545
+ ```ruby
546
+ save = Hytale.saves.first
547
+ map = save.map
548
+ ```
549
+
550
+ **Fast mode (default):** uniform color per chunk:
551
+
552
+ ```ruby
553
+ map.render_to_png("/tmp/map_fast.png")
554
+ ```
555
+
556
+ **Detailed mode:** per-block accuracy:
557
+
558
+ ```ruby
559
+ map.render_to_png("/tmp/map_detailed.png", detailed: true)
560
+ ```
561
+
562
+ **With scale (2x = 2 pixels per block):**
563
+
564
+ ```ruby
565
+ map.render_to_png("/tmp/map_2x.png", scale: 2, detailed: true)
566
+ ```
567
+
568
+ **Render a single region:**
569
+
570
+ ```ruby
571
+ region = map.regions.first
572
+ region.render_to_png("/tmp/region.png", scale: 2, detailed: true)
573
+ ```
574
+
575
+ **Render a single chunk:**
576
+
577
+ ```ruby
578
+ chunk = region.chunks.values.first
579
+ chunk.render_to_png("/tmp/chunk.png", scale: 4, detailed: true)
580
+ ```
581
+
582
+ **Using the Renderer directly:**
583
+
584
+ ```ruby
585
+ renderer = Hytale::Client::Map::Renderer.new
586
+ ```
587
+
588
+ **Get average color from a block's texture:**
589
+
590
+ ```ruby
591
+ renderer.block_color("Soil_Grass") # => ChunkyPNG color
592
+ ```
593
+
594
+ **Render with custom options:**
595
+
596
+ ```ruby
597
+ png = renderer.render_map(map, scale: 2, detailed: true)
598
+ png.save("/tmp/map.png")
599
+ ```
600
+
601
+ **Check cached colors:**
602
+
603
+ ```ruby
604
+ renderer.color_cache
605
+ # => {"Soil_Grass" => 7379500, "Rock_Stone" => 7894873, ...}
606
+ ```
607
+
608
+ **Color extraction:**
609
+
610
+ The renderer extracts average colors from block textures:
28
611
 
29
- After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
612
+ | Block Type | Color | Source Texture |
613
+ |------------|-------|----------------|
614
+ | Soil_Grass | `#709C2C` | Soil_Grass_Sunny.png |
615
+ | Soil_Dirt | `#8A652C` | Soil_Dirt.png |
616
+ | Rock_Stone | `#787759` | Rock_Stone.png |
617
+ | Soil_Sand | `#CFA643` | Soil_Sand.png |
30
618
 
31
- To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
619
+ Blocks without textures use sensible defaults (e.g., blue for water).
620
+
621
+ **Chunk analysis:**
622
+
623
+ ```ruby
624
+ chunk = region.each_chunk.first
625
+
626
+ chunk.block_types # => ["Rock_Stone", "Soil_Dirt", "Soil_Grass", ...]
627
+ chunk.terrain_type # => :grassland
628
+ chunk.has_water? # => false
629
+ chunk.has_vegetation? # => true
630
+ chunk.local_x # => 15
631
+ chunk.local_z # => 8
632
+ chunk.world_x # => -272
633
+ chunk.world_z # => -408
634
+ ```
635
+
636
+ **ASCII representation:**
637
+
638
+ ```ruby
639
+ puts chunk.to_ascii_map
640
+ # GGGGGGGGGGGGGGGG
641
+ # GGGGGGGGGGGGGGGG
642
+ # ...
643
+ ```
644
+
645
+ ### Backups
646
+
647
+ Access automatic backup files:
648
+
649
+ ```ruby
650
+ save.backups.each do |backup|
651
+ puts "#{backup.filename} - #{backup.size_mb} MB"
652
+ puts "Created: #{backup.created_at}"
653
+ end
654
+ ```
655
+
656
+ ### Launcher Log
657
+
658
+ Parse the Hytale launcher log:
659
+
660
+ **Current state:**
661
+
662
+ ```ruby
663
+ log = Hytale.launcher_log
664
+
665
+ log.current_version # => "2026.01.13-b6c7e88"
666
+ log.current_channel # => "release"
667
+ log.current_profile_uuid # => "79816d74-..."
668
+ ```
669
+
670
+ **Game launches:**
671
+
672
+ ```ruby
673
+ log.game_launches.count # => 6
674
+ ```
675
+
676
+ **Errors:**
677
+
678
+ ```ruby
679
+ log.errors.each do |entry|
680
+ puts "[#{entry.timestamp}] #{entry.message}"
681
+ end
682
+ ```
683
+
684
+ **Sessions:**
685
+
686
+ ```ruby
687
+ log.sessions.each do |session|
688
+ puts "#{session.started_at} - v#{session.version}"
689
+ puts " Game launched: #{session.game_launched?}"
690
+ puts " Errors: #{session.errors.count}"
691
+ end
692
+ ```
693
+
694
+ **Log entry types:**
695
+
696
+ | Method | Description |
697
+ |--------|-------------|
698
+ | `entries` | All log entries |
699
+ | `errors` | Error-level entries |
700
+ | `warnings` | Warning-level entries |
701
+ | `info` | Info-level entries |
702
+ | `game_launches` | Game start events |
703
+ | `updates` | Update events |
704
+ | `sessions` | Grouped by launcher start |
705
+
706
+ ### Custom Data Path
707
+
708
+ Override the default data path:
709
+
710
+ **Set custom path:**
711
+
712
+ ```ruby
713
+ Hytale::Client.data_path = "/path/to/hytale/data"
714
+ ```
715
+
716
+ **Reset to platform default:**
717
+
718
+ ```ruby
719
+ Hytale::Client::Config.reset!
720
+ ```
721
+
722
+ ### Platform Support
723
+
724
+ The gem automatically detects the Hytale data directory:
725
+
726
+ | Platform | Default Path |
727
+ |----------|-------------|
728
+ | macOS | `~/Library/Application Support/Hytale` |
729
+ | Windows | `%APPDATA%/Hytale` |
730
+ | Linux | `~/.local/share/Hytale` |
731
+
732
+ ### Process Detection
733
+
734
+ Detect if the Hytale client is running:
735
+
736
+ **Check if game is running:**
737
+
738
+ ```ruby
739
+ Hytale.client.running?
740
+ # => true
741
+ ```
742
+
743
+ **List running client processes:**
744
+
745
+ ```ruby
746
+ Hytale::Client::Process.list
747
+ # => [#<Hytale::Client::Process pid=12345>]
748
+ ```
749
+
750
+ **Check a specific process:**
751
+
752
+ ```ruby
753
+ process = Hytale::Client::Process.list.first
754
+ process.running? # => true
755
+ process.pid # => 12345
756
+ ```
757
+
758
+ ## API Reference
759
+
760
+ ### Hytale (Top-level)
761
+
762
+ | Method | Description |
763
+ |--------|-------------|
764
+ | `Hytale.client` | Access client module |
765
+ | `Hytale.server` | Access server module |
766
+ | `Hytale.settings` | Load game settings |
767
+ | `Hytale.saves` | List all saves |
768
+ | `Hytale.players` | List all players across all saves |
769
+ | `Hytale.launcher_log` | Load launcher log |
770
+
771
+ ### Hytale::Client
772
+
773
+ | Method | Description |
774
+ |--------|-------------|
775
+ | `installed?` | Check if Hytale is installed |
776
+ | `running?` | Check if Hytale client is running |
777
+ | `processes` | List running client processes |
778
+ | `data_path` | Get/set data directory |
779
+ | `settings` | Load Settings |
780
+ | `saves` | List all Save objects |
781
+ | `save(name)` | Find Save by name |
782
+ | `launcher_log` | Load LauncherLog |
783
+ | `prefabs` | List all Prefab objects |
784
+ | `prefab(name)` | Find Prefab by name |
785
+ | `prefab_categories` | List prefab category names |
786
+ | `prefabs_in_category(name)` | List prefabs in a category |
787
+ | `block_types` | List all BlockType objects |
788
+ | `block_type(id)` | Create BlockType by ID |
789
+ | `block_type_categories` | List block category names |
790
+ | `block_types_in_category(name)` | List block types in a category |
791
+ | `players` | List all players across all saves |
792
+ | `player(uuid)` | Find Player by UUID |
793
+ | `player_skins` | List all cached PlayerSkin objects |
794
+ | `player_skin(uuid)` | Find PlayerSkin by UUID |
795
+
796
+ ### Hytale::Client::Map
797
+
798
+ | Method | Description |
799
+ |--------|-------------|
800
+ | `regions` | All Region objects |
801
+ | `region_at(x, z)` | Find region by region coordinates |
802
+ | `region_at_world(x, z)` | Find region by world coordinates |
803
+ | `chunk_at(x, z)` | Get chunk at world coordinates |
804
+ | `block_at(x, y, z)` | Get Block at world coordinates |
805
+ | `world_to_region_coords(x, z)` | Convert world → region coords |
806
+ | `world_to_chunk_local_coords(x, z)` | Convert world → chunk-local coords |
807
+ | `world_to_block_local_coords(x, z)` | Convert world → block-local coords |
808
+ | `bounds` | Map boundaries (min/max x/z) |
809
+ | `markers` | Map markers (discovered locations) |
810
+ | `block_types` | All block types across regions |
811
+ | `total_size_mb` | Total size of all region files |
812
+ | `render_to_png(path, scale:, detailed:)` | Render map to PNG image |
813
+ | `to_ascii(players:)` | ASCII representation |
814
+
815
+ ### Hytale::Client::Map::Region
816
+
817
+ | Method | Description |
818
+ |--------|-------------|
819
+ | `x`, `z` | Region coordinates |
820
+ | `chunk_count` | Number of non-empty chunks |
821
+ | `chunk_exists?(x, z)` | Check if chunk exists at local coords |
822
+ | `chunk_at_index(idx)` | Get chunk by index (0-1023) |
823
+ | `each_chunk` | Iterate over all chunks |
824
+ | `block_types` | All block types in region |
825
+ | `render_to_png(path, scale:, detailed:)` | Render region to PNG image |
826
+
827
+ ### Hytale::Client::Map::Chunk
828
+
829
+ | Method | Description |
830
+ |--------|-------------|
831
+ | `index` | Chunk index in region (0-1023) |
832
+ | `local_x`, `local_z` | Position within region (0-31) |
833
+ | `world_x`, `world_z` | World coordinates |
834
+ | `size` | Decompressed data size in bytes |
835
+ | `height` | Number of Y layers in chunk |
836
+ | `block_at(x, y, z)` | Get Block instance at local coordinates |
837
+ | `block_type_at(x, y, z)` | Get block type ID string (faster) |
838
+ | `surface_at(x, z)` | Find highest non-empty Block at X, Z |
839
+ | `block_types` | Block type IDs found in chunk |
840
+ | `block_palette` | Parsed palette (index → block name) |
841
+ | `terrain_type` | Detected terrain (`:grassland`, `:water`, etc.) |
842
+ | `has_water?` | Contains water blocks |
843
+ | `has_vegetation?` | Contains plant/grass blocks |
844
+ | `to_ascii_map` | 16x16 ASCII representation |
845
+
846
+ ### Hytale::Client::Map::Block
847
+
848
+ | Method | Description |
849
+ |--------|-------------|
850
+ | `id` | Block type ID (e.g., "Rock_Stone") |
851
+ | `name` | Human-readable name |
852
+ | `category` | Block category (e.g., "Rock") |
853
+ | `block_type` | Associated BlockType instance |
854
+ | `x`, `y`, `z` | Local coordinates within chunk |
855
+ | `world_x`, `world_y`, `world_z` | World coordinates |
856
+ | `local_position` | `[x, y, z]` array |
857
+ | `world_position` | `[world_x, world_y, world_z]` array |
858
+ | `chunk` | Parent Chunk reference |
859
+ | `empty?` | Is air/empty block? |
860
+ | `solid?` | Is solid (not empty, not liquid)? |
861
+ | `liquid?` | Is water/lava? |
862
+ | `vegetation?` | Is plant/grass? |
863
+ | `texture_path` | Path to texture file |
864
+ | `texture_exists?` | Does texture exist? |
865
+ | `texture_data` | Raw PNG texture data |
866
+
867
+ ### Hytale::Client::BlockType
868
+
869
+ | Method | Description |
870
+ |--------|-------------|
871
+ | `id` | Block type ID (e.g., "Rock_Stone") |
872
+ | `name` | Human-readable name |
873
+ | `category` | Block category (e.g., "Rock") |
874
+ | `subcategory` | Block subcategory if available |
875
+ | `texture_name` | Texture filename |
876
+ | `texture_path` | Path to texture file |
877
+ | `texture_exists?` | Does texture exist? |
878
+ | `texture_data` | Raw PNG texture data |
879
+ | `all_textures` | (class method) List all texture names |
880
+
881
+ ### Hytale::Client::Map::Renderer
882
+
883
+ | Method | Description |
884
+ |--------|-------------|
885
+ | `block_color(type)` | Get average color for block type |
886
+ | `render_chunk(chunk, scale:, detailed:)` | Render chunk to ChunkyPNG image |
887
+ | `render_region(region, scale:, detailed:)` | Render region to ChunkyPNG image |
888
+ | `render_map(map, scale:, detailed:)` | Render map to ChunkyPNG image |
889
+ | `save_region(region, path, scale:, detailed:)` | Save region PNG to file |
890
+ | `save_map(map, path, scale:, detailed:)` | Save map PNG to file |
891
+ | `color_cache` | Hash of cached block colors |
892
+
893
+ ## Technical Details
894
+
895
+ ### Region File Format (`.region.bin`)
896
+
897
+ Region files use the `HytaleIndexedStorage` format:
898
+
899
+ **Header (32 bytes):**
900
+
901
+ | Offset | Size | Description |
902
+ |--------|------|-------------|
903
+ | 0 | 20 | Magic: "HytaleIndexedStorage" |
904
+ | 20 | 4 | Version (BE) = 1 |
905
+ | 24 | 4 | Chunk count (BE) = 1024 |
906
+ | 28 | 4 | Index table size (BE) = 4096 |
907
+
908
+ **Index Table (4096 bytes):**
909
+
910
+ - 1024 entries of 4 bytes each (big-endian)
911
+ - Non-zero value indicates chunk exists
912
+
913
+ **Data Section:**
914
+
915
+ - Chunks stored at 4096-byte aligned positions
916
+ - Each chunk: `[decompressed_size 4B BE] [compressed_size 4B BE] [ZSTD data]`
917
+ - ZSTD magic: `0x28B52FFD`
918
+
919
+ **Decompression:**
920
+
921
+ ```ruby
922
+ require "zstd-ruby"
923
+ region = Hytale::Client::Map::Region.new(path)
924
+ region.block_types # Extracts block palette from chunks
925
+ ```
926
+
927
+ ### Chunk Data Format
928
+
929
+ Decompressed chunk data uses a BSON-like structure with numbered sections (0-9) containing block data.
930
+
931
+ **Block Data Section:**
932
+
933
+ | Offset | Size | Description |
934
+ |--------|------|-------------|
935
+ | 0 | 3 | Zeros (padding) |
936
+ | 3 | 1 | Type marker (0x0A) |
937
+ | 4 | 1 | Version (0x01) |
938
+ | 5 | 1 | Zero |
939
+ | 6 | 1 | Palette count |
940
+ | 7 | 2 | Zeros |
941
+ | 9 | N | Palette entries |
942
+ | 9+N | M | Block data (4-bit packed) |
943
+
944
+ **Palette Entry Format:**
945
+
946
+ | Size | Description |
947
+ |------|-------------|
948
+ | 1 | String length |
949
+ | N | Block name (e.g., "Rock_Stone") |
950
+ | 4 | Metadata (palette index at byte 2) |
951
+
952
+ **Block Data Encoding:**
953
+
954
+ - 4-bit packed indices (2 blocks per byte)
955
+ - 128 bytes per Y layer (16×16 blocks)
956
+ - Low nibble = block at even position
957
+ - High nibble = block at odd position
958
+
959
+ **Accessing blocks:**
960
+
961
+ **Block at (x, y, z) within chunk:**
962
+
963
+ ```ruby
964
+ layer_offset = y * 128
965
+ block_index = z * 16 + x
966
+ byte_offset = layer_offset + (block_index / 2)
967
+ ```
968
+
969
+ **Extract 4-bit index:**
970
+
971
+ ```ruby
972
+ if block_index.even?
973
+ palette_index = byte & 0x0F # Low nibble
974
+ else
975
+ palette_index = (byte >> 4) & 0x0F # High nibble
976
+ end
977
+
978
+ block_name = palette[palette_index]
979
+ ```
980
+
981
+ ### Prefab File Format (`.prefab.json.lpf`)
982
+
983
+ Prefab files store pre-built structures (trees, buildings, dungeons, etc.) in a custom binary format. Despite the `.json` in the filename, these are binary files, not JSON.
984
+
985
+ **Header (21 bytes):**
986
+
987
+ | Offset | Size | Description |
988
+ |--------|------|-------------|
989
+ | 0 | 2 | Palette offset (BE) = 21 |
990
+ | 2 | 2 | Header value (BE) |
991
+ | 4 | 10 | Reserved/dimensions |
992
+ | 14 | 2 | Palette count (BE) |
993
+ | 16 | 5 | Reserved |
994
+
995
+ **Block Palette:**
996
+
997
+ Each palette entry:
998
+
999
+ | Size | Description |
1000
+ |------|-------------|
1001
+ | 1 | String length |
1002
+ | N | Block name (ASCII) |
1003
+ | 2 | Flags (BE) |
1004
+ | 2 | Block ID (BE) |
1005
+ | 1 | Extra data (rotation/state) |
1006
+
1007
+ **Placement Data:**
1008
+
1009
+ Block placement coordinates follow the palette. Format varies by prefab complexity.
1010
+
1011
+ **List all prefabs:**
1012
+
1013
+ ```ruby
1014
+ Hytale.client.prefabs.each do |prefab|
1015
+ puts "#{prefab.name}: #{prefab.palette.size} block types"
1016
+ end
1017
+ ```
1018
+
1019
+ **Get prefab categories:**
1020
+
1021
+ ```ruby
1022
+ Hytale.client.prefab_categories
1023
+ # => ["Cave", "Dungeon", "Mineshaft", "Monuments", "Npc", "Plants", ...]
1024
+ ```
1025
+
1026
+ **Find prefabs by category:**
1027
+
1028
+ ```ruby
1029
+ Hytale.client.prefabs_in_category("Trees")
1030
+ ```
1031
+
1032
+ **Find specific prefab:**
1033
+
1034
+ ```ruby
1035
+ prefab = Hytale.client.prefab("Burnt_dead_Stage2_005")
1036
+ prefab.name # => "Burnt_dead_Stage2_005"
1037
+ prefab.category # => "Trees"
1038
+ prefab.block_names # => ["Wood_Burnt_Branch_Long", "Wood_Burnt_Trunk", ...]
1039
+ ```
1040
+
1041
+ **Access palette entries:**
1042
+
1043
+ ```ruby
1044
+ prefab.palette.each do |entry|
1045
+ puts "#{entry.name} (ID: 0x#{format('%04X', entry.block_id)})"
1046
+ end
1047
+ ```
1048
+
1049
+ ### Blocks and Block Types
1050
+
1051
+ The gem distinguishes between:
1052
+ - **BlockType** - A block definition (e.g., "Rock_Stone") with texture and category info
1053
+ - **Block** - A specific block at coordinates in the world, referencing its BlockType
1054
+
1055
+ **BlockType - Block definitions:**
1056
+
1057
+ ```ruby
1058
+ Hytale.client.block_types.count # => 1156
1059
+ Hytale.client.block_type_categories # => ["Alchemy", "Bench", "Ore", "Plant", "Rock", ...]
1060
+ ```
1061
+
1062
+ **Get block types by category:**
1063
+
1064
+ ```ruby
1065
+ Hytale.client.block_types_in_category("Ore")
1066
+ # => [BlockType: Ore_Copper_Stone, BlockType: Ore_Iron_Stone, ...]
1067
+ ```
1068
+
1069
+ **Create a block type directly:**
1070
+
1071
+ ```ruby
1072
+ block_type = Hytale.client.block_type("Rock_Stone")
1073
+ block_type.id # => "Rock_Stone"
1074
+ block_type.name # => "Rock Stone"
1075
+ block_type.category # => "Rock"
1076
+ ```
1077
+
1078
+ **BlockType textures:**
1079
+
1080
+ ```ruby
1081
+ block_type.texture_path # => "/path/to/gem/assets/Common/BlockTextures/Rock_Stone.png"
1082
+ block_type.texture_exists? # => true
1083
+ block_type.texture_data # => PNG binary data
1084
+ ```
1085
+
1086
+ **List all available textures:**
1087
+
1088
+ ```ruby
1089
+ Hytale::Client::BlockType.all_textures
1090
+ # => ["Bone_Side", "Bone_Top", "Calcite", ...]
1091
+ ```
1092
+
1093
+ **Block - Positioned blocks in the world:**
1094
+
1095
+ ```ruby
1096
+ chunk = region.chunks.values.first
1097
+ ```
1098
+
1099
+ **Get a block at specific coordinates:**
1100
+
1101
+ ```ruby
1102
+ block = chunk.block_at(8, 50, 8)
1103
+ block.id # => "Rock_Quartzite"
1104
+ block.name # => "Rock Quartzite"
1105
+ block.category # => "Rock"
1106
+ block.block_type # => BlockType instance
1107
+ ```
1108
+
1109
+ **Local position within chunk:**
1110
+
1111
+ ```ruby
1112
+ block.x # => 8
1113
+ block.y # => 50
1114
+ block.z # => 8
1115
+ block.local_position # => [8, 50, 8]
1116
+ ```
1117
+
1118
+ **World coordinates:**
1119
+
1120
+ ```ruby
1121
+ block.world_x # => -8
1122
+ block.world_y # => 50
1123
+ block.world_z # => -280
1124
+ block.world_position # => [-8, 50, -280]
1125
+ ```
1126
+
1127
+ **Block properties:**
1128
+
1129
+ ```ruby
1130
+ block.empty? # => false
1131
+ block.solid? # => true
1132
+ block.liquid? # => false
1133
+ block.vegetation? # => false
1134
+ ```
1135
+
1136
+ **Access texture through block_type:**
1137
+
1138
+ ```ruby
1139
+ block.texture_path # => "/path/to/Rock_Quartzite.png"
1140
+ block.texture_exists? # => true
1141
+ ```
1142
+
1143
+ **Finding surface blocks:**
1144
+
1145
+ ```ruby
1146
+ surface = chunk.surface_at(8, 8)
1147
+ surface.id # => "Rock_Bedrock"
1148
+ surface.y # => 126
1149
+ surface.world_position # => [-8, 126, -280]
1150
+ ```
1151
+
1152
+ **Performance note:** For bulk operations, use `block_type_at(x, y, z)` which returns just the string ID without creating Block instances:
1153
+
1154
+ ```ruby
1155
+ type_id = chunk.block_type_at(8, 50, 8) # => "Rock_Quartzite"
1156
+ ```
1157
+
1158
+ ### Player Skins
1159
+
1160
+ Access cached player skin/cosmetic data:
1161
+
1162
+ **List all cached skins:**
1163
+
1164
+ ```ruby
1165
+ Hytale.client.player_skins
1166
+ # => [PlayerSkin: 79816d74-..., ...]
1167
+ ```
1168
+
1169
+ **Find a specific skin:**
1170
+
1171
+ ```ruby
1172
+ skin = Hytale.client.player_skin("79816d74-0500-4dad-9767-06af86c17243")
1173
+ skin.uuid # => "79816d74-0500-4dad-9767-06af86c17243"
1174
+ ```
1175
+
1176
+ **Appearance:**
1177
+
1178
+ ```ruby
1179
+ skin.body_characteristic # => "Muscular.06"
1180
+ skin.face # => "Face_Stubble"
1181
+ skin.eyes # => "Medium_Eyes.BrownDark"
1182
+ skin.haircut # => "VikinManBun.BrownDark"
1183
+ skin.facial_hair # => "Groomed_Large.BrownDark"
1184
+ skin.eyebrows # => "Square.BrownDark"
1185
+ ```
1186
+
1187
+ **Clothing:**
1188
+
1189
+ ```ruby
1190
+ skin.pants # => "BulkySuede.Brown"
1191
+ skin.overpants # => "LongSocks_Bow.Pink"
1192
+ skin.undertop # => "VikingShirt.Black"
1193
+ skin.overtop # => nil
1194
+ skin.shoes # => "HeavyLeather.Black"
1195
+ skin.gloves # => "FlowerBracer.Gold_Red"
1196
+ skin.cape # => nil
1197
+ ```
1198
+
1199
+ **Accessories:**
1200
+
1201
+ ```ruby
1202
+ skin.head_accessory # => nil
1203
+ skin.face_accessory # => nil
1204
+ skin.ear_accessory # => "SimpleEarring.Gold_Red.Right"
1205
+ ```
1206
+
1207
+ **Utility methods:**
1208
+
1209
+ ```ruby
1210
+ skin.equipped_items # => {"bodyCharacteristic" => "Muscular.06", ...}
1211
+ skin.empty_slots # => ["overtop", "headAccessory", "cape", ...]
1212
+ ```
1213
+
1214
+ **Avatar preview:**
1215
+
1216
+ ```ruby
1217
+ skin.avatar_preview_path # => "/path/to/CachedAvatarPreviews/uuid.png"
1218
+ skin.avatar_preview_data # => PNG binary data
1219
+ ```
1220
+
1221
+ **Texture paths:**
1222
+
1223
+ ```ruby
1224
+ skin.haircut_texture_path # => "/path/to/assets/Common/Characters/Haircuts/Viking_Topknot_Greyscale.png"
1225
+ skin.pants_texture_path # => "/path/to/assets/Common/Cosmetics/Pants/Pants_Brown.png"
1226
+ skin.shoes_texture_path # => "/path/to/assets/Common/Cosmetics/Shoes/HeavyLeather_Black.png"
1227
+ ```
1228
+
1229
+ **Get all texture paths at once:**
1230
+
1231
+ ```ruby
1232
+ skin.texture_paths
1233
+ # => {haircut: "/path/to/...", pants: "/path/to/...", ...}
1234
+ ```
1235
+
1236
+ ### Cosmetics
1237
+
1238
+ Access the cosmetic item catalog:
1239
+
1240
+ **Look up cosmetic items:**
1241
+
1242
+ ```ruby
1243
+ Hytale::Client::Cosmetics.find(:haircuts, "VikinManBun")
1244
+ # => {"Id" => "VikinManBun", "Model" => "...", "GreyscaleTexture" => "..."}
1245
+
1246
+ Hytale::Client::Cosmetics.find(:pants, "BulkySuede")
1247
+ # => {"Id" => "BulkySuede", "Model" => "...", "Textures" => {...}}
1248
+ ```
1249
+
1250
+ **Get texture and model paths:**
1251
+
1252
+ ```ruby
1253
+ Hytale::Client::Cosmetics.texture_path(:haircuts, "VikinManBun.BrownDark")
1254
+ # => "/path/to/assets/Common/Characters/Haircuts/Viking_Topknot_Greyscale.png"
1255
+
1256
+ Hytale::Client::Cosmetics.model_path(:haircuts, "VikinManBun")
1257
+ # => "/path/to/assets/Common/Characters/Haircuts/Viking_TopKnot.blockymodel"
1258
+ ```
1259
+
1260
+ **Available cosmetic types:**
1261
+
1262
+ | Type | Description |
1263
+ |------|-------------|
1264
+ | `:haircuts` | Hair styles |
1265
+ | `:facial_hair` | Beards, mustaches |
1266
+ | `:eyebrows` | Eyebrow styles |
1267
+ | `:eyes` | Eye styles |
1268
+ | `:faces` | Face textures |
1269
+ | `:pants` | Pants/bottoms |
1270
+ | `:overpants` | Socks, leg accessories |
1271
+ | `:undertops` | Shirts, undershirts |
1272
+ | `:overtops` | Jackets, vests |
1273
+ | `:shoes` | Footwear |
1274
+ | `:gloves` | Gloves, bracers |
1275
+ | `:capes` | Capes |
1276
+ | `:head_accessories` | Hats, helmets |
1277
+ | `:face_accessories` | Glasses, masks |
1278
+ | `:ear_accessories` | Earrings |
1279
+
1280
+ ### Assets
1281
+
1282
+ The gem automatically extracts and caches all game assets from `Assets.zip` on first use.
1283
+
1284
+ **Asset cache location:**
1285
+
1286
+ ```ruby
1287
+ Hytale::Client::Assets.cache_path # => "/path/to/gem/assets"
1288
+ Hytale::Client::Assets.count # => 57708
1289
+ ```
1290
+
1291
+ **List asset directories:**
1292
+
1293
+ ```ruby
1294
+ Hytale::Client::Assets.directories
1295
+ # => ["Common/BlockTextures", "Common/Blocks", "Common/Items", "Server/Prefabs", ...]
1296
+ ```
1297
+
1298
+ **Access any asset:**
1299
+
1300
+ ```ruby
1301
+ Hytale::Client::Assets.cached?("Common/Icons/Item_Sword_Copper.png") # => true
1302
+ Hytale::Client::Assets.read("Common/Icons/Item_Sword_Copper.png") # => PNG binary data
1303
+ Hytale::Client::Assets.cached_path("Common/Icons/Item_Sword_Copper.png")
1304
+ # => "/path/to/gem/assets/Common/Icons/Item_Sword_Copper.png"
1305
+ ```
1306
+
1307
+ **List files in a directory:**
1308
+
1309
+ ```ruby
1310
+ Hytale::Client::Assets.list("Common/Icons")
1311
+ # => ["Common/Icons/Item_Sword_Copper.png", ...]
1312
+ ```
1313
+
1314
+ **Clear the cache:**
1315
+
1316
+ ```ruby
1317
+ Hytale::Client::Assets.clear!
1318
+ ```
1319
+
1320
+ ## Development
1321
+
1322
+ ```bash
1323
+ git clone https://github.com/marcoroth/hytale-ruby
1324
+ cd hytale
1325
+ bundle install
1326
+ bundle exec rake test
1327
+ ```
32
1328
 
33
1329
  ## Contributing
34
1330
 
35
- Bug reports and pull requests are welcome on GitHub at https://github.com/marcoroth/hytale. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/marcoroth/hytale/blob/main/CODE_OF_CONDUCT.md).
1331
+ Bug reports and pull requests are welcome on GitHub at https://github.com/marcoroth/hytale-ruby.
1332
+
1333
+ ## License
1334
+
1335
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
36
1336
 
37
- ## Code of Conduct
1337
+ ## Disclaimer
38
1338
 
39
- Everyone interacting in the Hytale project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/marcoroth/hytale/blob/main/CODE_OF_CONDUCT.md).
1339
+ This gem is not affiliated with or endorsed by Hypixel Studios.