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/README.md CHANGED
@@ -1,39 +1,1454 @@
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-ruby"></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:**
32
+
33
+ ```bash
34
+ hytale info
35
+ ```
36
+
37
+ **List all saves:**
38
+
39
+ ```bash
40
+ hytale saves
41
+ ```
42
+
43
+ **Show save details:**
12
44
 
13
45
  ```bash
14
- bundle add UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG
46
+ hytale save "New World"
15
47
  ```
16
48
 
17
- If bundler is not being used to manage dependencies, install the gem by executing:
49
+ **Show player details:**
18
50
 
19
51
  ```bash
20
- gem install UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG
52
+ hytale player "New World" marcoroth
21
53
  ```
22
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 # => "00000000-0000-0000-0000-000000000000"
299
+ player.position # => (590.75, 123.0, 374.2)
300
+ player.game_mode # => "Adventure"
301
+ player.discovered_zones # => [Zone::Region, Zone::Region, ...]
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
+ | `backpack` | Optional backpack storage (if equipped) |
337
+ | `armor` | Head, chest, hands, legs |
338
+ | `utility` | Utility items (4 slots) |
339
+ | `tools` | Builder/editor tools |
340
+
341
+ **Check if player has a backpack:**
342
+
343
+ ```ruby
344
+ player.inventory.backpack? # => true/false
345
+ ```
346
+
347
+ **ItemStorage type checks:**
348
+
349
+ ```ruby
350
+ player.inventory.backpack.empty? # => true (type is "Empty" - no backpack equipped)
351
+ player.inventory.backpack.simple? # => true (type is "Simple" - backpack equipped)
352
+ ```
353
+
354
+ **Item properties:**
355
+
356
+ ```ruby
357
+ item.id # => "Tool_Pickaxe_Copper"
358
+ item.name # => "Tool Pickaxe Copper"
359
+ item.quantity # => 1
360
+ item.durability # => 29.75
361
+ item.max_durability # => 200.0
362
+ item.durability_percent # => 14.9
363
+ item.damaged? # => true
364
+ ```
365
+
366
+ ### Zones and Regions
367
+
368
+ Hytale organizes the world into zones (biomes) and regions (areas within zones):
369
+
370
+ **Zones (biomes):**
371
+
372
+ **List all zones (requires game to be installed):**
373
+
374
+ ```ruby
375
+ Hytale::Client::Zone.all
376
+ # => [#<Zone id="Emerald_Wilds">, #<Zone id="Howling_Sands">, ...]
377
+ ```
378
+
379
+ **Find a specific zone:**
380
+
381
+ ```ruby
382
+ zone = Hytale::Client::Zone.find("Emerald_Wilds")
383
+ zone.id # => "Emerald_Wilds"
384
+ zone.name # => "Emerald Wilds"
385
+ ```
386
+
387
+ **Get all regions in a zone:**
388
+
389
+ ```ruby
390
+ zone.regions
391
+ # => [#<Zone::Region id="Zone1_Spawn">, #<Zone::Region id="Zone1_Tier1">, ...]
392
+ ```
393
+
394
+ **Regions (areas within zones):**
395
+
396
+ **List all regions:**
397
+
398
+ ```ruby
399
+ Hytale::Client::Zone::Region.all
400
+ # => [#<Zone::Region id="Zone1_Spawn">, #<Zone::Region id="Zone1_Tier1">, ...]
401
+ ```
402
+
403
+ **Find a specific region:**
404
+
405
+ ```ruby
406
+ region = Hytale::Client::Zone::Region.find("Zone1_Tier1")
407
+ region.id # => "Zone1_Tier1"
408
+ region.name # => "Drifting Plains"
409
+ region.region_name # => "Drifting Plains"
410
+ ```
411
+
412
+ **Navigate to parent zone:**
413
+
414
+ ```ruby
415
+ region.zone # => #<Zone id="Emerald_Wilds">
416
+ region.zone.name # => "Emerald Wilds"
417
+ ```
418
+
419
+ **Player discovered zones:**
420
+
421
+ ```ruby
422
+ player.discovered_zones.each do |region|
423
+ puts "#{region.name} (#{region.zone.name})"
424
+ end
425
+ # => First Gate of the Echo (Emerald Wilds)
426
+ # => Drifting Plains (Emerald Wilds)
427
+ ```
428
+
429
+ **Zone/Region mapping:**
430
+
431
+ | Zone | Region Prefix | Example Regions |
432
+ |------|---------------|-----------------|
433
+ | Emerald Wilds | Zone1_* | Zone1_Spawn, Zone1_Tier1, Zone1_Tier2, Zone1_Tier3 |
434
+ | Howling Sands | Zone2_* | Zone2_Tier1, Zone2_Tier2, Zone2_Tier3 |
435
+ | Whisperfrost Frontiers | Zone3_* | Zone3_Tier1, Zone3_Tier2, Zone3_Tier3 |
436
+ | Devastated Lands | Zone4_* | Zone4_Tier4, Zone4_Tier5 |
437
+ | Oceans | Oceans | Oceans |
438
+
439
+ ### Memories (Discovered Creatures)
440
+
441
+ Track discovered NPCs and creatures:
442
+
443
+ **Count and list:**
444
+
445
+ ```ruby
446
+ memories = save.memories
447
+ memories.count # => 42
448
+ ```
449
+
450
+ **List all discovered roles:**
451
+
452
+ ```ruby
453
+ memories.roles
454
+ # => ["Bat", "Bear_Grizzly", "Bluebird", "Boar", ...]
455
+ ```
456
+
457
+ **List discovery locations:**
458
+
459
+ ```ruby
460
+ memories.locations
461
+ # => ["ForgottenTemple", "Zone1_Tier1", "Zone1_Tier2", ...]
462
+ ```
463
+
464
+ **Find specific creatures:**
465
+
466
+ ```ruby
467
+ memories.find_by_role("Wolf_Black")
468
+ # => Memory: Wolf Black found at Zone1_Tier1
469
+
470
+ memories.find_all_by_location("ForgottenTemple")
471
+ # => [Memory: Duck, Memory: Kweebec_Rootling, ...]
472
+ ```
473
+
474
+ **Iterate:**
475
+
476
+ ```ruby
477
+ memories.each do |memory|
478
+ puts "#{memory.friendly_name} at #{memory.location} (#{memory.captured_at})"
479
+ end
480
+ ```
481
+
482
+ ### Permissions
483
+
484
+ Read server permissions and groups:
485
+
486
+ **List groups:**
487
+
488
+ ```ruby
489
+ permissions = save.permissions
490
+
491
+ permissions.groups
492
+ # => {"Default" => [], "OP" => ["*"]}
493
+ ```
494
+
495
+ **Check user permissions:**
496
+
497
+ ```ruby
498
+ permissions.user_groups(uuid)
499
+ # => ["Adventure"]
500
+
501
+ permissions.op?(uuid)
502
+ # => false
503
+ ```
504
+
505
+ ### Map Data
506
+
507
+ Access explored regions and map markers:
508
+
509
+ **Get map for a save:**
510
+
511
+ ```ruby
512
+ map = save.map
513
+ ```
514
+
515
+ **Region coverage:**
516
+
517
+ ```ruby
518
+ map.regions.count # => 10
519
+ map.total_size_mb # => 120.56
520
+ map.bounds # => {min_x: -1, max_x: 2, min_z: -1, max_z: 1, ...}
521
+ ```
522
+
523
+ **Individual regions:**
524
+
525
+ ```ruby
526
+ map.regions.each do |region|
527
+ puts "#{region.x}, #{region.z}: #{region.size_mb} MB"
528
+ end
529
+ ```
530
+
531
+ **Region details:**
532
+
533
+ ```ruby
534
+ region = map.regions.first
535
+ region.header # => {version: 1, chunk_count: 1024, ...}
536
+ region.chunk_count # => 207 (non-empty chunks)
537
+ region.block_types # => ["Rock_Stone", "Soil_Grass", ...]
538
+ ```
539
+
540
+ **Block types across all regions:**
541
+
542
+ ```ruby
543
+ map.block_types # => ["Ore_Copper", "Plant_Bush", "Rock_Stone", ...]
544
+ ```
545
+
546
+ **Map markers (discovered locations):**
547
+
548
+ ```ruby
549
+ map.markers.each do |marker|
550
+ puts "#{marker.name} at #{marker.position}"
551
+ end
552
+ # => "Forgotten Temple Portal Enter at (832, 113, 367)"
553
+ ```
554
+
555
+ **ASCII map of explored regions:**
556
+
557
+ ```ruby
558
+ puts map.to_ascii(players: save.players)
559
+ ```
560
+
561
+ ```
562
+ -1 0 1 2
563
+ --------------
564
+ -1 | o O O |
565
+ 0 | O # M . |
566
+ 1 | o O o |
567
+ --------------
568
+
569
+ Legend: . = small, o = medium, O = large, # = huge, Letter = player
570
+ ```
571
+
572
+ ### Global Coordinates
573
+
574
+ Access regions, chunks, and blocks using world coordinates:
575
+
576
+ ```ruby
577
+ map = save.map
578
+ ```
579
+
580
+ **Get region containing world coordinates:**
581
+
582
+ ```ruby
583
+ region = map.region_at_world(100, 200)
584
+ ```
585
+
586
+ **Get chunk at world coordinates:**
587
+
588
+ ```ruby
589
+ chunk = map.chunk_at(100, 200)
590
+ ```
591
+
592
+ **Get block at world coordinates:**
593
+
594
+ ```ruby
595
+ block = map.block_at(100, 50, 200)
596
+ block.id # => "Rock_Stone"
597
+ block.world_position # => [100, 50, 200]
598
+ ```
599
+
600
+ **Coordinate conversion helpers:**
601
+
602
+ ```ruby
603
+ map.world_to_region_coords(100, 200) # => [0, 0]
604
+ map.world_to_region_coords(-100, -200) # => [-1, -1]
605
+ map.world_to_chunk_local_coords(100, 200) # => [6, 12]
606
+ map.world_to_block_local_coords(100, 200) # => [4, 8]
607
+ ```
608
+
609
+ **Coordinate system:**
610
+
611
+ | Unit | Size | Description |
612
+ |------|------|-------------|
613
+ | Block | 1 | Smallest unit |
614
+ | Chunk | 16×16 blocks | Vertical column of blocks |
615
+ | Region | 32×32 chunks (512×512 blocks) | Stored in `.region.bin` files |
616
+
617
+ Region 0 covers blocks 0..511, region -1 covers -512..-1, etc.
618
+
619
+ ### Map Rendering
620
+
621
+ Generate PNG images of maps using colors derived from block textures.
622
+
623
+ **Render modes:**
624
+
625
+ | Mode | Description | Speed |
626
+ |------|-------------|-------|
627
+ | Fast (`detailed: false`) | Colors entire chunk with dominant surface block | ~5s for 10 regions |
628
+ | Detailed (`detailed: true`) | Renders each block individually using `surface_at` | ~15s for 10 regions |
629
+
630
+ **Render a map to PNG:**
631
+
632
+ ```ruby
633
+ save = Hytale.saves.first
634
+ map = save.map
635
+ ```
636
+
637
+ **Fast mode (default):** uniform color per chunk:
638
+
639
+ ```ruby
640
+ map.render_to_png("/tmp/map_fast.png")
641
+ ```
642
+
643
+ **Detailed mode:** per-block accuracy:
644
+
645
+ ```ruby
646
+ map.render_to_png("/tmp/map_detailed.png", detailed: true)
647
+ ```
648
+
649
+ **With scale (2x = 2 pixels per block):**
650
+
651
+ ```ruby
652
+ map.render_to_png("/tmp/map_2x.png", scale: 2, detailed: true)
653
+ ```
654
+
655
+ **Render a single region:**
656
+
657
+ ```ruby
658
+ region = map.regions.first
659
+ region.render_to_png("/tmp/region.png", scale: 2, detailed: true)
660
+ ```
661
+
662
+ **Render a single chunk:**
663
+
664
+ ```ruby
665
+ chunk = region.chunks.values.first
666
+ chunk.render_to_png("/tmp/chunk.png", scale: 4, detailed: true)
667
+ ```
28
668
 
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.
669
+ **Using the Renderer directly:**
30
670
 
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).
671
+ ```ruby
672
+ renderer = Hytale::Client::Map::Renderer.new
673
+ ```
674
+
675
+ **Get average color from a block's texture:**
676
+
677
+ ```ruby
678
+ renderer.block_color("Soil_Grass") # => ChunkyPNG color
679
+ ```
680
+
681
+ **Render with custom options:**
682
+
683
+ ```ruby
684
+ png = renderer.render_map(map, scale: 2, detailed: true)
685
+ png.save("/tmp/map.png")
686
+ ```
687
+
688
+ **Check cached colors:**
689
+
690
+ ```ruby
691
+ renderer.color_cache
692
+ # => {"Soil_Grass" => 7379500, "Rock_Stone" => 7894873, ...}
693
+ ```
694
+
695
+ **Color extraction:**
696
+
697
+ The renderer extracts average colors from block textures:
698
+
699
+ | Block Type | Color | Source Texture |
700
+ |------------|-------|----------------|
701
+ | Soil_Grass | `#709C2C` | Soil_Grass_Sunny.png |
702
+ | Soil_Dirt | `#8A652C` | Soil_Dirt.png |
703
+ | Rock_Stone | `#787759` | Rock_Stone.png |
704
+ | Soil_Sand | `#CFA643` | Soil_Sand.png |
705
+
706
+ Blocks without textures use sensible defaults (e.g., blue for water).
707
+
708
+ **Chunk analysis:**
709
+
710
+ ```ruby
711
+ chunk = region.each_chunk.first
712
+
713
+ chunk.block_types # => ["Rock_Stone", "Soil_Dirt", "Soil_Grass", ...]
714
+ chunk.terrain_type # => :grassland
715
+ chunk.water? # => false
716
+ chunk.vegetation? # => true
717
+ chunk.local_x # => 15
718
+ chunk.local_z # => 8
719
+ chunk.world_x # => -272
720
+ chunk.world_z # => -408
721
+ ```
722
+
723
+ **ASCII representation:**
724
+
725
+ ```ruby
726
+ puts chunk.to_ascii_map
727
+ # GGGGGGGGGGGGGGGG
728
+ # GGGGGGGGGGGGGGGG
729
+ # ...
730
+ ```
731
+
732
+ ### Backups
733
+
734
+ Access automatic backup files:
735
+
736
+ ```ruby
737
+ save.backups.each do |backup|
738
+ puts "#{backup.filename} - #{backup.size_mb} MB"
739
+ puts "Created: #{backup.created_at}"
740
+ end
741
+ ```
742
+
743
+ ### Launcher Log
744
+
745
+ Parse the Hytale launcher log:
746
+
747
+ **Current state:**
748
+
749
+ ```ruby
750
+ log = Hytale.launcher_log
751
+
752
+ log.current_version # => "2026.01.13-b6c7e88"
753
+ log.current_channel # => "release"
754
+ log.current_profile_uuid # => "79816d74-..."
755
+ ```
756
+
757
+ **Game launches:**
758
+
759
+ ```ruby
760
+ log.game_launches.count # => 6
761
+ ```
762
+
763
+ **Errors:**
764
+
765
+ ```ruby
766
+ log.errors.each do |entry|
767
+ puts "[#{entry.timestamp}] #{entry.message}"
768
+ end
769
+ ```
770
+
771
+ **Sessions:**
772
+
773
+ ```ruby
774
+ log.sessions.each do |session|
775
+ puts "#{session.started_at} - v#{session.version}"
776
+ puts " Game launched: #{session.game_launched?}"
777
+ puts " Errors: #{session.errors.count}"
778
+ end
779
+ ```
780
+
781
+ **Log entry types:**
782
+
783
+ | Method | Description |
784
+ |--------|-------------|
785
+ | `entries` | All log entries |
786
+ | `errors` | Error-level entries |
787
+ | `warnings` | Warning-level entries |
788
+ | `info` | Info-level entries |
789
+ | `game_launches` | Game start events |
790
+ | `updates` | Update events |
791
+ | `sessions` | Grouped by launcher start |
792
+
793
+ ### Custom Data Path
794
+
795
+ Override the default data path:
796
+
797
+ **Set custom path:**
798
+
799
+ ```ruby
800
+ Hytale::Client.data_path = "/path/to/hytale/data"
801
+ ```
802
+
803
+ **Reset to platform default:**
804
+
805
+ ```ruby
806
+ Hytale::Client::Config.reset!
807
+ ```
808
+
809
+ ### Platform Support
810
+
811
+ The gem automatically detects the Hytale data directory:
812
+
813
+ | Platform | Default Path |
814
+ |----------|-------------|
815
+ | macOS | `~/Library/Application Support/Hytale` |
816
+ | Windows | `%APPDATA%/Hytale` |
817
+ | Linux | `~/.local/share/Hytale` |
818
+
819
+ ### Process Detection
820
+
821
+ Detect if the Hytale client is running:
822
+
823
+ **Check if game is running:**
824
+
825
+ ```ruby
826
+ Hytale.client.running?
827
+ # => true
828
+ ```
829
+
830
+ **List running client processes:**
831
+
832
+ ```ruby
833
+ Hytale::Client::Process.list
834
+ # => [#<Hytale::Client::Process pid=12345>]
835
+ ```
836
+
837
+ **Check a specific process:**
838
+
839
+ ```ruby
840
+ process = Hytale::Client::Process.list.first
841
+ process.running? # => true
842
+ process.pid # => 12345
843
+ ```
844
+
845
+ ## API Reference
846
+
847
+ ### Hytale (Top-level)
848
+
849
+ | Method | Description |
850
+ |--------|-------------|
851
+ | `Hytale.client` | Access client module |
852
+ | `Hytale.server` | Access server module |
853
+ | `Hytale.settings` | Load game settings |
854
+ | `Hytale.saves` | List all saves |
855
+ | `Hytale.players` | List all players across all saves |
856
+ | `Hytale.launcher_log` | Load launcher log |
857
+
858
+ ### Hytale::Client
859
+
860
+ | Method | Description |
861
+ |--------|-------------|
862
+ | `installed?` | Check if Hytale is installed |
863
+ | `running?` | Check if Hytale client is running |
864
+ | `processes` | List running client processes |
865
+ | `data_path` | Get/set data directory |
866
+ | `settings` | Load Settings |
867
+ | `saves` | List all Save objects |
868
+ | `save(name)` | Find Save by name |
869
+ | `launcher_log` | Load LauncherLog |
870
+ | `prefabs` | List all Prefab objects |
871
+ | `prefab(name)` | Find Prefab by name |
872
+ | `prefab_categories` | List prefab category names |
873
+ | `prefabs_in_category(name)` | List prefabs in a category |
874
+ | `block_types` | List all BlockType objects |
875
+ | `block_type(id)` | Create BlockType by ID |
876
+ | `block_type_categories` | List block category names |
877
+ | `block_types_in_category(name)` | List block types in a category |
878
+ | `players` | List all players across all saves |
879
+ | `player(uuid)` | Find Player by UUID |
880
+ | `player_skins` | List all cached PlayerSkin objects |
881
+ | `player_skin(uuid)` | Find PlayerSkin by UUID |
882
+
883
+ ### Hytale::Client::Zone
884
+
885
+ | Method | Description |
886
+ |--------|-------------|
887
+ | `all` | List all Zone objects (from locale) |
888
+ | `find(id)` | Find Zone by ID, returns nil if not found |
889
+ | `new(id)` | Create a Zone::Base instance |
890
+
891
+ ### Hytale::Client::Zone::Base
892
+
893
+ | Method | Description |
894
+ |--------|-------------|
895
+ | `id` | Zone ID (e.g., "Emerald_Wilds") |
896
+ | `name` | Translated zone name (e.g., "Emerald Wilds") |
897
+ | `regions` | All Region objects belonging to this zone |
898
+
899
+ ### Hytale::Client::Zone::Region
900
+
901
+ | Method | Description |
902
+ |--------|-------------|
903
+ | `all` | List all Region objects (from locale) |
904
+ | `find(id)` | Find Region by ID, returns nil if not found |
905
+ | `id` | Region ID (e.g., "Zone1_Tier1") |
906
+ | `name` | Translated region name (e.g., "Drifting Plains") |
907
+ | `region_name` | Same as `name` |
908
+ | `zone` | Parent Zone::Base object |
909
+ | `zone_name` | Parent zone's translated name |
910
+
911
+ ### Hytale::Client::Map
912
+
913
+ | Method | Description |
914
+ |--------|-------------|
915
+ | `regions` | All Region objects |
916
+ | `region_at(x, z)` | Find region by region coordinates |
917
+ | `region_at_world(x, z)` | Find region by world coordinates |
918
+ | `chunk_at(x, z)` | Get chunk at world coordinates |
919
+ | `block_at(x, y, z)` | Get Block at world coordinates |
920
+ | `world_to_region_coords(x, z)` | Convert world → region coords |
921
+ | `world_to_chunk_local_coords(x, z)` | Convert world → chunk-local coords |
922
+ | `world_to_block_local_coords(x, z)` | Convert world → block-local coords |
923
+ | `bounds` | Map boundaries (min/max x/z) |
924
+ | `markers` | Map markers (discovered locations) |
925
+ | `block_types` | All block types across regions |
926
+ | `total_size_mb` | Total size of all region files |
927
+ | `render_to_png(path, scale:, detailed:)` | Render map to PNG image |
928
+ | `to_ascii(players:)` | ASCII representation |
929
+
930
+ ### Hytale::Client::Map::Region
931
+
932
+ | Method | Description |
933
+ |--------|-------------|
934
+ | `x`, `z` | Region coordinates |
935
+ | `chunk_count` | Number of non-empty chunks |
936
+ | `chunk_exists?(x, z)` | Check if chunk exists at local coords |
937
+ | `chunk_at_index(idx)` | Get chunk by index (0-1023) |
938
+ | `each_chunk` | Iterate over all chunks |
939
+ | `block_types` | All block types in region |
940
+ | `render_to_png(path, scale:, detailed:)` | Render region to PNG image |
941
+
942
+ ### Hytale::Client::Map::Chunk
943
+
944
+ | Method | Description |
945
+ |--------|-------------|
946
+ | `index` | Chunk index in region (0-1023) |
947
+ | `local_x`, `local_z` | Position within region (0-31) |
948
+ | `world_x`, `world_z` | World coordinates |
949
+ | `size` | Decompressed data size in bytes |
950
+ | `height` | Number of Y layers in chunk |
951
+ | `block_at(x, y, z)` | Get Block instance at local coordinates |
952
+ | `block_type_at(x, y, z)` | Get block type ID string (faster) |
953
+ | `surface_at(x, z)` | Find highest non-empty Block at X, Z |
954
+ | `block_types` | Block type IDs found in chunk |
955
+ | `block_palette` | Parsed palette (index → block name) |
956
+ | `terrain_type` | Detected terrain (`:grassland`, `:water`, etc.) |
957
+ | `water?` | Contains water blocks |
958
+ | `vegetation?` | Contains plant/grass blocks |
959
+ | `to_ascii_map` | 16x16 ASCII representation |
960
+
961
+ ### Hytale::Client::Map::Block
962
+
963
+ | Method | Description |
964
+ |--------|-------------|
965
+ | `id` | Block type ID (e.g., "Rock_Stone") |
966
+ | `name` | Human-readable name |
967
+ | `category` | Block category (e.g., "Rock") |
968
+ | `block_type` | Associated BlockType instance |
969
+ | `x`, `y`, `z` | Local coordinates within chunk |
970
+ | `world_x`, `world_y`, `world_z` | World coordinates |
971
+ | `local_position` | `[x, y, z]` array |
972
+ | `world_position` | `[world_x, world_y, world_z]` array |
973
+ | `chunk` | Parent Chunk reference |
974
+ | `empty?` | Is air/empty block? |
975
+ | `solid?` | Is solid (not empty, not liquid)? |
976
+ | `liquid?` | Is water/lava? |
977
+ | `vegetation?` | Is plant/grass? |
978
+ | `texture_path` | Path to texture file |
979
+ | `texture_exists?` | Does texture exist? |
980
+ | `texture_data` | Raw PNG texture data |
981
+
982
+ ### Hytale::Client::BlockType
983
+
984
+ | Method | Description |
985
+ |--------|-------------|
986
+ | `id` | Block type ID (e.g., "Rock_Stone") |
987
+ | `name` | Human-readable name |
988
+ | `category` | Block category (e.g., "Rock") |
989
+ | `subcategory` | Block subcategory if available |
990
+ | `texture_name` | Texture filename |
991
+ | `texture_path` | Path to texture file |
992
+ | `texture_exists?` | Does texture exist? |
993
+ | `texture_data` | Raw PNG texture data |
994
+ | `all_textures` | (class method) List all texture names |
995
+
996
+ ### Hytale::Client::Map::Renderer
997
+
998
+ | Method | Description |
999
+ |--------|-------------|
1000
+ | `block_color(type)` | Get average color for block type |
1001
+ | `render_chunk(chunk, scale:, detailed:)` | Render chunk to ChunkyPNG image |
1002
+ | `render_region(region, scale:, detailed:)` | Render region to ChunkyPNG image |
1003
+ | `render_map(map, scale:, detailed:)` | Render map to ChunkyPNG image |
1004
+ | `save_region(region, path, scale:, detailed:)` | Save region PNG to file |
1005
+ | `save_map(map, path, scale:, detailed:)` | Save map PNG to file |
1006
+ | `color_cache` | Hash of cached block colors |
1007
+
1008
+ ## Technical Details
1009
+
1010
+ ### Region File Format (`.region.bin`)
1011
+
1012
+ Region files use the `HytaleIndexedStorage` format:
1013
+
1014
+ **Header (32 bytes):**
1015
+
1016
+ | Offset | Size | Description |
1017
+ |--------|------|-------------|
1018
+ | 0 | 20 | Magic: "HytaleIndexedStorage" |
1019
+ | 20 | 4 | Version (BE) = 1 |
1020
+ | 24 | 4 | Chunk count (BE) = 1024 |
1021
+ | 28 | 4 | Index table size (BE) = 4096 |
1022
+
1023
+ **Index Table (4096 bytes):**
1024
+
1025
+ - 1024 entries of 4 bytes each (big-endian)
1026
+ - Non-zero value indicates chunk exists
1027
+
1028
+ **Data Section:**
1029
+
1030
+ - Chunks stored at 4096-byte aligned positions
1031
+ - Each chunk: `[decompressed_size 4B BE] [compressed_size 4B BE] [ZSTD data]`
1032
+ - ZSTD magic: `0x28B52FFD`
1033
+
1034
+ **Decompression:**
1035
+
1036
+ ```ruby
1037
+ require "zstd-ruby"
1038
+ region = Hytale::Client::Map::Region.new(path)
1039
+ region.block_types # Extracts block palette from chunks
1040
+ ```
1041
+
1042
+ ### Chunk Data Format
1043
+
1044
+ Decompressed chunk data uses a BSON-like structure with numbered sections (0-9) containing block data.
1045
+
1046
+ **Block Data Section:**
1047
+
1048
+ | Offset | Size | Description |
1049
+ |--------|------|-------------|
1050
+ | 0 | 3 | Zeros (padding) |
1051
+ | 3 | 1 | Type marker (0x0A) |
1052
+ | 4 | 1 | Version (0x01) |
1053
+ | 5 | 1 | Zero |
1054
+ | 6 | 1 | Palette count |
1055
+ | 7 | 2 | Zeros |
1056
+ | 9 | N | Palette entries |
1057
+ | 9+N | M | Block data (4-bit packed) |
1058
+
1059
+ **Palette Entry Format:**
1060
+
1061
+ | Size | Description |
1062
+ |------|-------------|
1063
+ | 1 | String length |
1064
+ | N | Block name (e.g., "Rock_Stone") |
1065
+ | 4 | Metadata (palette index at byte 2) |
1066
+
1067
+ **Block Data Encoding:**
1068
+
1069
+ - 4-bit packed indices (2 blocks per byte)
1070
+ - 128 bytes per Y layer (16×16 blocks)
1071
+ - Low nibble = block at even position
1072
+ - High nibble = block at odd position
1073
+
1074
+ **Accessing blocks:**
1075
+
1076
+ **Block at (x, y, z) within chunk:**
1077
+
1078
+ ```ruby
1079
+ layer_offset = y * 128
1080
+ block_index = z * 16 + x
1081
+ byte_offset = layer_offset + (block_index / 2)
1082
+ ```
1083
+
1084
+ **Extract 4-bit index:**
1085
+
1086
+ ```ruby
1087
+ if block_index.even?
1088
+ palette_index = byte & 0x0F # Low nibble
1089
+ else
1090
+ palette_index = (byte >> 4) & 0x0F # High nibble
1091
+ end
1092
+
1093
+ block_name = palette[palette_index]
1094
+ ```
1095
+
1096
+ ### Prefab File Format (`.prefab.json.lpf`)
1097
+
1098
+ 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.
1099
+
1100
+ **Header (21 bytes):**
1101
+
1102
+ | Offset | Size | Description |
1103
+ |--------|------|-------------|
1104
+ | 0 | 2 | Palette offset (BE) = 21 |
1105
+ | 2 | 2 | Header value (BE) |
1106
+ | 4 | 10 | Reserved/dimensions |
1107
+ | 14 | 2 | Palette count (BE) |
1108
+ | 16 | 5 | Reserved |
1109
+
1110
+ **Block Palette:**
1111
+
1112
+ Each palette entry:
1113
+
1114
+ | Size | Description |
1115
+ |------|-------------|
1116
+ | 1 | String length |
1117
+ | N | Block name (ASCII) |
1118
+ | 2 | Flags (BE) |
1119
+ | 2 | Block ID (BE) |
1120
+ | 1 | Extra data (rotation/state) |
1121
+
1122
+ **Placement Data:**
1123
+
1124
+ Block placement coordinates follow the palette. Format varies by prefab complexity.
1125
+
1126
+ **List all prefabs:**
1127
+
1128
+ ```ruby
1129
+ Hytale.client.prefabs.each do |prefab|
1130
+ puts "#{prefab.name}: #{prefab.palette.size} block types"
1131
+ end
1132
+ ```
1133
+
1134
+ **Get prefab categories:**
1135
+
1136
+ ```ruby
1137
+ Hytale.client.prefab_categories
1138
+ # => ["Cave", "Dungeon", "Mineshaft", "Monuments", "Npc", "Plants", ...]
1139
+ ```
1140
+
1141
+ **Find prefabs by category:**
1142
+
1143
+ ```ruby
1144
+ Hytale.client.prefabs_in_category("Trees")
1145
+ ```
1146
+
1147
+ **Find specific prefab:**
1148
+
1149
+ ```ruby
1150
+ prefab = Hytale.client.prefab("Burnt_dead_Stage2_005")
1151
+ prefab.name # => "Burnt_dead_Stage2_005"
1152
+ prefab.category # => "Trees"
1153
+ prefab.block_names # => ["Wood_Burnt_Branch_Long", "Wood_Burnt_Trunk", ...]
1154
+ ```
1155
+
1156
+ **Access palette entries:**
1157
+
1158
+ ```ruby
1159
+ prefab.palette.each do |entry|
1160
+ puts "#{entry.name} (ID: 0x#{format('%04X', entry.block_id)})"
1161
+ end
1162
+ ```
1163
+
1164
+ ### Blocks and Block Types
1165
+
1166
+ The gem distinguishes between:
1167
+ - **BlockType** - A block definition (e.g., "Rock_Stone") with texture and category info
1168
+ - **Block** - A specific block at coordinates in the world, referencing its BlockType
1169
+
1170
+ **BlockType - Block definitions:**
1171
+
1172
+ ```ruby
1173
+ Hytale.client.block_types.count # => 1156
1174
+ Hytale.client.block_type_categories # => ["Alchemy", "Bench", "Ore", "Plant", "Rock", ...]
1175
+ ```
1176
+
1177
+ **Get block types by category:**
1178
+
1179
+ ```ruby
1180
+ Hytale.client.block_types_in_category("Ore")
1181
+ # => [BlockType: Ore_Copper_Stone, BlockType: Ore_Iron_Stone, ...]
1182
+ ```
1183
+
1184
+ **Create a block type directly:**
1185
+
1186
+ ```ruby
1187
+ block_type = Hytale.client.block_type("Rock_Stone")
1188
+ block_type.id # => "Rock_Stone"
1189
+ block_type.name # => "Rock Stone"
1190
+ block_type.category # => "Rock"
1191
+ ```
1192
+
1193
+ **BlockType textures:**
1194
+
1195
+ ```ruby
1196
+ block_type.texture_path # => "/path/to/gem/assets/Common/BlockTextures/Rock_Stone.png"
1197
+ block_type.texture_exists? # => true
1198
+ block_type.texture_data # => PNG binary data
1199
+ ```
1200
+
1201
+ **List all available textures:**
1202
+
1203
+ ```ruby
1204
+ Hytale::Client::BlockType.all_textures
1205
+ # => ["Bone_Side", "Bone_Top", "Calcite", ...]
1206
+ ```
1207
+
1208
+ **Block - Positioned blocks in the world:**
1209
+
1210
+ ```ruby
1211
+ chunk = region.chunks.values.first
1212
+ ```
1213
+
1214
+ **Get a block at specific coordinates:**
1215
+
1216
+ ```ruby
1217
+ block = chunk.block_at(8, 50, 8)
1218
+ block.id # => "Rock_Quartzite"
1219
+ block.name # => "Rock Quartzite"
1220
+ block.category # => "Rock"
1221
+ block.block_type # => BlockType instance
1222
+ ```
1223
+
1224
+ **Local position within chunk:**
1225
+
1226
+ ```ruby
1227
+ block.x # => 8
1228
+ block.y # => 50
1229
+ block.z # => 8
1230
+ block.local_position # => [8, 50, 8]
1231
+ ```
1232
+
1233
+ **World coordinates:**
1234
+
1235
+ ```ruby
1236
+ block.world_x # => -8
1237
+ block.world_y # => 50
1238
+ block.world_z # => -280
1239
+ block.world_position # => [-8, 50, -280]
1240
+ ```
1241
+
1242
+ **Block properties:**
1243
+
1244
+ ```ruby
1245
+ block.empty? # => false
1246
+ block.solid? # => true
1247
+ block.liquid? # => false
1248
+ block.vegetation? # => false
1249
+ ```
1250
+
1251
+ **Access texture through block_type:**
1252
+
1253
+ ```ruby
1254
+ block.texture_path # => "/path/to/Rock_Quartzite.png"
1255
+ block.texture_exists? # => true
1256
+ ```
1257
+
1258
+ **Finding surface blocks:**
1259
+
1260
+ ```ruby
1261
+ surface = chunk.surface_at(8, 8)
1262
+ surface.id # => "Rock_Bedrock"
1263
+ surface.y # => 126
1264
+ surface.world_position # => [-8, 126, -280]
1265
+ ```
1266
+
1267
+ **Performance note:** For bulk operations, use `block_type_at(x, y, z)` which returns just the string ID without creating Block instances:
1268
+
1269
+ ```ruby
1270
+ type_id = chunk.block_type_at(8, 50, 8) # => "Rock_Quartzite"
1271
+ ```
1272
+
1273
+ ### Player Skins
1274
+
1275
+ Access cached player skin/cosmetic data:
1276
+
1277
+ **List all cached skins:**
1278
+
1279
+ ```ruby
1280
+ Hytale.client.player_skins
1281
+ # => [PlayerSkin: 79816d74-..., ...]
1282
+ ```
1283
+
1284
+ **Find a specific skin:**
1285
+
1286
+ ```ruby
1287
+ skin = Hytale.client.player_skin("00000000-0000-0000-0000-000000000000")
1288
+ skin.uuid # => "00000000-0000-0000-0000-000000000000"
1289
+ ```
1290
+
1291
+ **Appearance:**
1292
+
1293
+ ```ruby
1294
+ skin.body_characteristic # => "Muscular.06"
1295
+ skin.face # => "Face_Stubble"
1296
+ skin.eyes # => "Medium_Eyes.BrownDark"
1297
+ skin.haircut # => "VikinManBun.BrownDark"
1298
+ skin.facial_hair # => "Groomed_Large.BrownDark"
1299
+ skin.eyebrows # => "Square.BrownDark"
1300
+ ```
1301
+
1302
+ **Clothing:**
1303
+
1304
+ ```ruby
1305
+ skin.pants # => "BulkySuede.Brown"
1306
+ skin.overpants # => "LongSocks_Bow.Pink"
1307
+ skin.undertop # => "VikingShirt.Black"
1308
+ skin.overtop # => nil
1309
+ skin.shoes # => "HeavyLeather.Black"
1310
+ skin.gloves # => "FlowerBracer.Gold_Red"
1311
+ skin.cape # => nil
1312
+ ```
1313
+
1314
+ **Accessories:**
1315
+
1316
+ ```ruby
1317
+ skin.head_accessory # => nil
1318
+ skin.face_accessory # => nil
1319
+ skin.ear_accessory # => "SimpleEarring.Gold_Red.Right"
1320
+ ```
1321
+
1322
+ **Utility methods:**
1323
+
1324
+ ```ruby
1325
+ skin.equipped_items # => {"bodyCharacteristic" => "Muscular.06", ...}
1326
+ skin.empty_slots # => ["overtop", "headAccessory", "cape", ...]
1327
+ ```
1328
+
1329
+ **Avatar preview:**
1330
+
1331
+ ```ruby
1332
+ skin.avatar_preview_path # => "/path/to/CachedAvatarPreviews/uuid.png"
1333
+ skin.avatar_preview_data # => PNG binary data
1334
+ ```
1335
+
1336
+ **Texture paths:**
1337
+
1338
+ ```ruby
1339
+ skin.haircut_texture_path # => "/path/to/assets/Common/Characters/Haircuts/Viking_Topknot_Greyscale.png"
1340
+ skin.pants_texture_path # => "/path/to/assets/Common/Cosmetics/Pants/Pants_Brown.png"
1341
+ skin.shoes_texture_path # => "/path/to/assets/Common/Cosmetics/Shoes/HeavyLeather_Black.png"
1342
+ ```
1343
+
1344
+ **Get all texture paths at once:**
1345
+
1346
+ ```ruby
1347
+ skin.texture_paths
1348
+ # => {haircut: "/path/to/...", pants: "/path/to/...", ...}
1349
+ ```
1350
+
1351
+ ### Cosmetics
1352
+
1353
+ Access the cosmetic item catalog:
1354
+
1355
+ **Look up cosmetic items:**
1356
+
1357
+ ```ruby
1358
+ Hytale::Client::Cosmetics.find(:haircuts, "VikinManBun")
1359
+ # => {"Id" => "VikinManBun", "Model" => "...", "GreyscaleTexture" => "..."}
1360
+
1361
+ Hytale::Client::Cosmetics.find(:pants, "BulkySuede")
1362
+ # => {"Id" => "BulkySuede", "Model" => "...", "Textures" => {...}}
1363
+ ```
1364
+
1365
+ **Get texture and model paths:**
1366
+
1367
+ ```ruby
1368
+ Hytale::Client::Cosmetics.texture_path(:haircuts, "VikinManBun.BrownDark")
1369
+ # => "/path/to/assets/Common/Characters/Haircuts/Viking_Topknot_Greyscale.png"
1370
+
1371
+ Hytale::Client::Cosmetics.model_path(:haircuts, "VikinManBun")
1372
+ # => "/path/to/assets/Common/Characters/Haircuts/Viking_TopKnot.blockymodel"
1373
+ ```
1374
+
1375
+ **Available cosmetic types:**
1376
+
1377
+ | Type | Description |
1378
+ |------|-------------|
1379
+ | `:haircuts` | Hair styles |
1380
+ | `:facial_hair` | Beards, mustaches |
1381
+ | `:eyebrows` | Eyebrow styles |
1382
+ | `:eyes` | Eye styles |
1383
+ | `:faces` | Face textures |
1384
+ | `:pants` | Pants/bottoms |
1385
+ | `:overpants` | Socks, leg accessories |
1386
+ | `:undertops` | Shirts, undershirts |
1387
+ | `:overtops` | Jackets, vests |
1388
+ | `:shoes` | Footwear |
1389
+ | `:gloves` | Gloves, bracers |
1390
+ | `:capes` | Capes |
1391
+ | `:head_accessories` | Hats, helmets |
1392
+ | `:face_accessories` | Glasses, masks |
1393
+ | `:ear_accessories` | Earrings |
1394
+
1395
+ ### Assets
1396
+
1397
+ The gem automatically extracts and caches all game assets from `Assets.zip` on first use.
1398
+
1399
+ **Asset cache location:**
1400
+
1401
+ ```ruby
1402
+ Hytale::Client::Assets.cache_path # => "/path/to/gem/assets"
1403
+ Hytale::Client::Assets.count # => 57708
1404
+ ```
1405
+
1406
+ **List asset directories:**
1407
+
1408
+ ```ruby
1409
+ Hytale::Client::Assets.directories
1410
+ # => ["Common/BlockTextures", "Common/Blocks", "Common/Items", "Server/Prefabs", ...]
1411
+ ```
1412
+
1413
+ **Access any asset:**
1414
+
1415
+ ```ruby
1416
+ Hytale::Client::Assets.cached?("Common/Icons/Item_Sword_Copper.png") # => true
1417
+ Hytale::Client::Assets.read("Common/Icons/Item_Sword_Copper.png") # => PNG binary data
1418
+ Hytale::Client::Assets.cached_path("Common/Icons/Item_Sword_Copper.png")
1419
+ # => "/path/to/gem/assets/Common/Icons/Item_Sword_Copper.png"
1420
+ ```
1421
+
1422
+ **List files in a directory:**
1423
+
1424
+ ```ruby
1425
+ Hytale::Client::Assets.list("Common/Icons")
1426
+ # => ["Common/Icons/Item_Sword_Copper.png", ...]
1427
+ ```
1428
+
1429
+ **Clear the cache:**
1430
+
1431
+ ```ruby
1432
+ Hytale::Client::Assets.clear!
1433
+ ```
1434
+
1435
+ ## Development
1436
+
1437
+ ```bash
1438
+ git clone https://github.com/marcoroth/hytale-ruby
1439
+ cd hytale
1440
+ bundle install
1441
+ bundle exec rake test
1442
+ ```
32
1443
 
33
1444
  ## Contributing
34
1445
 
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).
1446
+ Bug reports and pull requests are welcome on GitHub at https://github.com/marcoroth/hytale-ruby.
1447
+
1448
+ ## License
1449
+
1450
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
36
1451
 
37
- ## Code of Conduct
1452
+ ## Disclaimer
38
1453
 
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).
1454
+ This gem is not affiliated with or endorsed by Hypixel Studios.