hytale 0.1.0 → 0.1.2

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ca11b549d14d2bd553261112ed5d9397d3521677ea5ecbd4595960c821e805e6
4
- data.tar.gz: 6f52dcc127393200cfdf3cd3332c5404dbfa168c02e3299b24c5e444450b4a1c
3
+ metadata.gz: c1ecc8d6da6ef59f252740739b05fad04f49e0a6cd628c94a085a6dbd43cb918
4
+ data.tar.gz: 804efc4f07dc64ac5a68ba5d28043c1ac0094f6c42ba6cf1734c7f1c985b4380
5
5
  SHA512:
6
- metadata.gz: 29a3bcf4aad9b01adc248bc014b2cede5221950b72152e3e18add25ba7858776885ddfaf799406f0da88ace286abb20577cd30e1786c37f4348783cb7c0db388
7
- data.tar.gz: 34f3c99f23607f0e5ef6577ca4a0699037591302bafb58865392d9cd4293cbaa878938e1eee8c222207407d179389620c2f07a77f9c5af6d772d82eb8b95ac75
6
+ metadata.gz: a1be72b9c7373e5fa9b682758b5b756384d25350df8b14fc5bdb555a09f9577f93ecd50b297b56c436e94dee76872c2d6972316d6b202f4b9ada06b335980aec
7
+ data.tar.gz: e73dc31ff553991792fafbc611d58c037770af922366075e27a1982294eb90618c58fb89cd38d552bfbcc394084f3a24f0eb0f163bf4f73e826a2f4604c8647d
data/README.md CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  <p>
6
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>
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
8
  </p>
9
9
 
10
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>
@@ -295,10 +295,10 @@ save = Hytale.client.save("New World")
295
295
  player = save.players.first
296
296
 
297
297
  player.name # => "marcoroth"
298
- player.uuid # => "79816d74-0500-4dad-9767-06af86c17243"
298
+ player.uuid # => "00000000-0000-0000-0000-000000000000"
299
299
  player.position # => (590.75, 123.0, 374.2)
300
300
  player.game_mode # => "Adventure"
301
- player.discovered_zones # => ["Zone1_Spawn", "Zone1_Tier1", ...]
301
+ player.discovered_zones # => [Zone::Region, Zone::Region, ...]
302
302
  player.skin # => PlayerSkin object
303
303
  player.avatar_preview_path # => "/path/to/CachedAvatarPreviews/uuid.png"
304
304
  ```
@@ -333,10 +333,24 @@ end
333
333
  |------|-------------|
334
334
  | `hotbar` | 9-slot quick access bar |
335
335
  | `storage` | Main inventory (36 slots) |
336
+ | `backpack` | Optional backpack storage (if equipped) |
336
337
  | `armor` | Head, chest, hands, legs |
337
338
  | `utility` | Utility items (4 slots) |
338
339
  | `tools` | Builder/editor tools |
339
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
+
340
354
  **Item properties:**
341
355
 
342
356
  ```ruby
@@ -349,6 +363,130 @@ item.durability_percent # => 14.9
349
363
  item.damaged? # => true
350
364
  ```
351
365
 
366
+ **Respawn points:**
367
+
368
+ ```ruby
369
+ player.respawn_points.each do |point|
370
+ puts "#{point.name} at #{point.position}"
371
+ end
372
+ # => Kweebec village at (-2150.5, 119.05, -403.1)
373
+ # => Bed at (-795.1, 121.05, 29.47)
374
+ ```
375
+
376
+ **Death positions:**
377
+
378
+ ```ruby
379
+ player.death_positions.each do |death|
380
+ puts "Died on day #{death.day} at #{death.position}"
381
+ end
382
+ # => Died on day 68 at (-677.67, 27.99, -153.48)
383
+ ```
384
+
385
+ **Discovered instances (dungeons, locations):**
386
+
387
+ ```ruby
388
+ player.discovered_instances
389
+ # => ["4781d0dd-5370-4962-a1fc-521ec7ff3e23", ...]
390
+ ```
391
+
392
+ **Player state:**
393
+
394
+ ```ruby
395
+ player.flying? # => false
396
+ player.first_spawn? # => false
397
+ player.head_rotation # => Rotation object (separate from body)
398
+ player.current_world # => "default"
399
+ ```
400
+
401
+ **Saved hotbars:**
402
+
403
+ ```ruby
404
+ player.saved_hotbars # => [ItemStorage, ...]
405
+ player.current_hotbar_index # => 0
406
+ ```
407
+
408
+ **Progress tracking:**
409
+
410
+ ```ruby
411
+ player.known_recipes # => []
412
+ player.unique_item_usages # => ["Upgrade_Backpack_1"]
413
+ player.active_objectives # => []
414
+ player.reputation_data # => {}
415
+ ```
416
+
417
+ ### Zones and Regions
418
+
419
+ Hytale organizes the world into zones (biomes) and regions (areas within zones):
420
+
421
+ **Zones (biomes):**
422
+
423
+ **List all zones (requires game to be installed):**
424
+
425
+ ```ruby
426
+ Hytale::Client::Zone.all
427
+ # => [#<Zone id="Emerald_Wilds">, #<Zone id="Howling_Sands">, ...]
428
+ ```
429
+
430
+ **Find a specific zone:**
431
+
432
+ ```ruby
433
+ zone = Hytale::Client::Zone.find("Emerald_Wilds")
434
+ zone.id # => "Emerald_Wilds"
435
+ zone.name # => "Emerald Wilds"
436
+ ```
437
+
438
+ **Get all regions in a zone:**
439
+
440
+ ```ruby
441
+ zone.regions
442
+ # => [#<Zone::Region id="Zone1_Spawn">, #<Zone::Region id="Zone1_Tier1">, ...]
443
+ ```
444
+
445
+ **Regions (areas within zones):**
446
+
447
+ **List all regions:**
448
+
449
+ ```ruby
450
+ Hytale::Client::Zone::Region.all
451
+ # => [#<Zone::Region id="Zone1_Spawn">, #<Zone::Region id="Zone1_Tier1">, ...]
452
+ ```
453
+
454
+ **Find a specific region:**
455
+
456
+ ```ruby
457
+ region = Hytale::Client::Zone::Region.find("Zone1_Tier1")
458
+ region.id # => "Zone1_Tier1"
459
+ region.name # => "Drifting Plains"
460
+ region.region_name # => "Drifting Plains"
461
+ ```
462
+
463
+ **Navigate to parent zone:**
464
+
465
+ ```ruby
466
+ region.zone # => #<Zone id="Emerald_Wilds">
467
+ region.zone.name # => "Emerald Wilds"
468
+ ```
469
+
470
+ **Player discovered zones:**
471
+
472
+ ```ruby
473
+ player.discovered_zones.each do |region|
474
+ puts "#{region.name} (#{region.zone.name})"
475
+ end
476
+ # => First Gate of the Echo (Emerald Wilds)
477
+ # => Drifting Plains (Emerald Wilds)
478
+ ```
479
+
480
+ **Zone/Region mapping:**
481
+
482
+ | Zone | Region Prefix | Example Regions |
483
+ |------|---------------|-----------------|
484
+ | Emerald Wilds | Zone1_* | Zone1_Spawn, Zone1_Tier1, Zone1_Tier2, Zone1_Tier3 |
485
+ | Howling Sands | Zone2_* | Zone2_Tier1, Zone2_Tier2, Zone2_Tier3 |
486
+ | Whisperfrost Frontiers | Zone3_* | Zone3_Tier1, Zone3_Tier2, Zone3_Tier3 |
487
+ | Devastated Lands | Zone4_* | Zone4_Tier4, Zone4_Tier5 |
488
+ | Oceans | Oceans | Oceans |
489
+
352
490
  ### Memories (Discovered Creatures)
353
491
 
354
492
  Track discovered NPCs and creatures:
@@ -625,8 +763,8 @@ chunk = region.each_chunk.first
625
763
 
626
764
  chunk.block_types # => ["Rock_Stone", "Soil_Dirt", "Soil_Grass", ...]
627
765
  chunk.terrain_type # => :grassland
628
- chunk.has_water? # => false
629
- chunk.has_vegetation? # => true
766
+ chunk.water? # => false
767
+ chunk.vegetation? # => true
630
768
  chunk.local_x # => 15
631
769
  chunk.local_z # => 8
632
770
  chunk.world_x # => -272
@@ -793,6 +931,83 @@ process.pid # => 12345
793
931
  | `player_skins` | List all cached PlayerSkin objects |
794
932
  | `player_skin(uuid)` | Find PlayerSkin by UUID |
795
933
 
934
+ ### Hytale::Client::Player
935
+
936
+ | Method | Description |
937
+ |--------|-------------|
938
+ | `name` | Player display name |
939
+ | `uuid` | Player UUID |
940
+ | `position` | Current Position object |
941
+ | `rotation` | Current body Rotation object |
942
+ | `head_rotation` | Current head Rotation object |
943
+ | `velocity` | Current Vector3 velocity |
944
+ | `stats` | EntityStats object (health, stamina, etc.) |
945
+ | `inventory` | Inventory object |
946
+ | `game_mode` | Current game mode ("Adventure", "Creative", etc.) |
947
+ | `current_world` | Current world name |
948
+ | `discovered_zones` | Array of Zone::Region objects |
949
+ | `discovered_instances` | Array of discovered dungeon/location UUIDs |
950
+ | `respawn_points` | Array of RespawnPoint objects |
951
+ | `death_positions` | Array of DeathPosition objects |
952
+ | `memories` | Array of PlayerMemory objects |
953
+ | `known_recipes` | Array of learned recipe IDs |
954
+ | `unique_item_usages` | Array of used item IDs (upgrades, etc.) |
955
+ | `active_objectives` | Array of active quest UUIDs |
956
+ | `reputation_data` | Hash of faction reputations |
957
+ | `saved_hotbars` | Array of saved ItemStorage hotbars |
958
+ | `current_hotbar_index` | Currently selected hotbar slot |
959
+ | `flying?` | Is player currently flying |
960
+ | `first_spawn?` | Has player spawned before |
961
+ | `skin` | PlayerSkin object |
962
+ | `avatar_preview_path` | Path to cached avatar image |
963
+
964
+ ### Hytale::Client::Player::RespawnPoint
965
+
966
+ | Method | Description |
967
+ |--------|-------------|
968
+ | `name` | Respawn point name (e.g., "Ria's bed") |
969
+ | `position` | Exact respawn Position |
970
+ | `block_position` | Block coordinates Position |
971
+ | `x`, `y`, `z` | Shortcut position accessors |
972
+
973
+ ### Hytale::Client::Player::DeathPosition
974
+
975
+ | Method | Description |
976
+ |--------|-------------|
977
+ | `marker_id` | Death marker UUID |
978
+ | `day` | Game day of death |
979
+ | `position` | Death Position object |
980
+ | `rotation` | Death Rotation object |
981
+ | `x`, `y`, `z` | Shortcut position accessors |
982
+
983
+ ### Hytale::Client::Zone
984
+
985
+ | Method | Description |
986
+ |--------|-------------|
987
+ | `all` | List all Zone objects (from locale) |
988
+ | `find(id)` | Find Zone by ID, returns nil if not found |
989
+ | `new(id)` | Create a Zone::Base instance |
990
+
991
+ ### Hytale::Client::Zone::Base
992
+
993
+ | Method | Description |
994
+ |--------|-------------|
995
+ | `id` | Zone ID (e.g., "Emerald_Wilds") |
996
+ | `name` | Translated zone name (e.g., "Emerald Wilds") |
997
+ | `regions` | All Region objects belonging to this zone |
998
+
999
+ ### Hytale::Client::Zone::Region
1000
+
1001
+ | Method | Description |
1002
+ |--------|-------------|
1003
+ | `all` | List all Region objects (from locale) |
1004
+ | `find(id)` | Find Region by ID, returns nil if not found |
1005
+ | `id` | Region ID (e.g., "Zone1_Tier1") |
1006
+ | `name` | Translated region name (e.g., "Drifting Plains") |
1007
+ | `region_name` | Same as `name` |
1008
+ | `zone` | Parent Zone::Base object |
1009
+ | `zone_name` | Parent zone's translated name |
1010
+
796
1011
  ### Hytale::Client::Map
797
1012
 
798
1013
  | Method | Description |
@@ -839,8 +1054,8 @@ process.pid # => 12345
839
1054
  | `block_types` | Block type IDs found in chunk |
840
1055
  | `block_palette` | Parsed palette (index → block name) |
841
1056
  | `terrain_type` | Detected terrain (`:grassland`, `:water`, etc.) |
842
- | `has_water?` | Contains water blocks |
843
- | `has_vegetation?` | Contains plant/grass blocks |
1057
+ | `water?` | Contains water blocks |
1058
+ | `vegetation?` | Contains plant/grass blocks |
844
1059
  | `to_ascii_map` | 16x16 ASCII representation |
845
1060
 
846
1061
  ### Hytale::Client::Map::Block
@@ -1169,8 +1384,8 @@ Hytale.client.player_skins
1169
1384
  **Find a specific skin:**
1170
1385
 
1171
1386
  ```ruby
1172
- skin = Hytale.client.player_skin("79816d74-0500-4dad-9767-06af86c17243")
1173
- skin.uuid # => "79816d74-0500-4dad-9767-06af86c17243"
1387
+ skin = Hytale.client.player_skin("00000000-0000-0000-0000-000000000000")
1388
+ skin.uuid # => "00000000-0000-0000-0000-000000000000"
1174
1389
  ```
1175
1390
 
1176
1391
  **Appearance:**
@@ -24,7 +24,9 @@ module Hytale
24
24
  def profile_uuid
25
25
  profile_entry = entries.find { |e| e.message&.include?("setting current profile") }
26
26
 
27
- profile_entry&.message&.match(/to (\S+)/)&.[](1)
27
+ return nil unless profile_entry
28
+
29
+ profile_entry.message&.match(/to (\S+)/)&.[](1)
28
30
  end
29
31
 
30
32
  def game_launched?
@@ -48,12 +48,18 @@ module Hytale
48
48
 
49
49
  def current_profile_uuid
50
50
  profile_entry = entries.reverse.find { |e| e.message&.include?("setting current profile") }
51
- profile_entry&.message&.match(/to (\S+)/)&.[](1)
51
+
52
+ return nil unless profile_entry
53
+
54
+ profile_entry.message&.match(/to (\S+)/)&.[](1)
52
55
  end
53
56
 
54
57
  def current_channel
55
58
  channel_entry = entries.reverse.find { |e| e.message&.include?("setting channel") }
56
- channel_entry&.attributes&.[]("channel")
59
+
60
+ return nil unless channel_entry
61
+
62
+ channel_entry.attributes&.[]("channel")
57
63
  end
58
64
 
59
65
  def last_game_launch
@@ -61,7 +67,6 @@ module Hytale
61
67
  end
62
68
 
63
69
  def sessions
64
- # Group entries by launcher start
65
70
  sessions = []
66
71
  current_session = nil
67
72
 
@@ -63,26 +63,26 @@ module Hytale
63
63
  @block_palette ||= extract_palette
64
64
  end
65
65
 
66
- def has_block?(block_type)
66
+ def block?(block_type)
67
67
  block_types.include?(block_type)
68
68
  end
69
69
 
70
- def has_water?
70
+ def water?
71
71
  block_types.any? { |t| t.include?("Water") }
72
72
  end
73
73
 
74
- def has_vegetation?
74
+ def vegetation?
75
75
  block_types.any? { |t| t.include?("Plant") || t.include?("Grass") || t.include?("Tree") }
76
76
  end
77
77
 
78
78
  def terrain_type
79
- if has_water?
79
+ if water?
80
80
  :water
81
81
  elsif block_types.any? { |t| t.include?("Sand") }
82
82
  :desert
83
83
  elsif block_types.any? { |t| t.include?("Snow") || t.include?("Ice") }
84
84
  :snow
85
- elsif has_vegetation?
85
+ elsif vegetation?
86
86
  :grassland
87
87
  else
88
88
  :rocky
@@ -195,9 +195,7 @@ module Hytale
195
195
  def cache_path(texture_scale: 16, shading: true)
196
196
  return nil unless region
197
197
 
198
- save_name = region.path.split("/").find do |p|
199
- p.include?("Saves")
200
- end&.then { |_| region.path.split("Saves/")[1]&.split("/")&.first } || "unknown"
198
+ save_name = region.path.split("/").find { |p| p.include?("Saves") }&.then { |_| region.path.split("Saves/")[1]&.split("/")&.first } || "unknown"
201
199
  world_name = region.path.split("/worlds/")[1]&.split("/")&.first || "default"
202
200
 
203
201
  cache_dir = File.join(
@@ -217,14 +215,17 @@ module Hytale
217
215
 
218
216
  def cached?(texture_scale: 16, shading: true)
219
217
  path = cache_path(texture_scale: texture_scale, shading: shading)
218
+
220
219
  path && File.exist?(path)
221
220
  end
222
221
 
223
222
  def clear_cache!
224
223
  path = cache_path
224
+
225
225
  return unless path
226
226
 
227
227
  dir = File.dirname(path)
228
+
228
229
  FileUtils.rm_rf(dir) if File.directory?(dir)
229
230
  end
230
231
 
@@ -238,6 +239,7 @@ module Hytale
238
239
  return nil unless type_id
239
240
 
240
241
  block_type = get_or_create_block_type(type_id)
242
+
241
243
  Block.new(block_type, x: x, y: y, z: z, chunk: self)
242
244
  end
243
245
 
@@ -643,7 +645,7 @@ module Hytale
643
645
  index_position = position + 1 + length + 2
644
646
  index = begin
645
647
  search_data[index_position].ord
646
- rescue StandardError
648
+ rescue StandardError # rubocop:disable Metrics/BlockNesting
647
649
  nil
648
650
  end
649
651
 
@@ -415,12 +415,10 @@ module Hytale
415
415
  ChunkyPNG::Color.rgb(194, 178, 128)
416
416
  when /Snow/, /Ice/
417
417
  ChunkyPNG::Color.rgb(240, 240, 255)
418
- when /Dirt/
418
+ when /Dirt/, /Wood/
419
419
  ChunkyPNG::Color.rgb(139, 90, 43)
420
420
  when /Stone/, /Rock/
421
421
  ChunkyPNG::Color.rgb(128, 128, 128)
422
- when /Wood/
423
- ChunkyPNG::Color.rgb(139, 90, 43)
424
422
  when /Plant/
425
423
  ChunkyPNG::Color.rgb(34, 139, 34)
426
424
  when /Ore/
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hytale
4
+ module Client
5
+ class Player
6
+ class DeathPosition
7
+ attr_reader :data
8
+
9
+ def initialize(data)
10
+ @data = data
11
+ end
12
+
13
+ def marker_id
14
+ data["MarkerId"]
15
+ end
16
+
17
+ def day
18
+ data["Day"]
19
+ end
20
+
21
+ def position
22
+ transform = data["Transform"] || {}
23
+
24
+ Position.new(transform["X"], transform["Y"], transform["Z"])
25
+ end
26
+
27
+ def rotation
28
+ transform = data["Transform"] || {}
29
+
30
+ Rotation.new(transform["Pitch"], transform["Yaw"], transform["Roll"])
31
+ end
32
+
33
+ def x = position.x
34
+ def y = position.y
35
+ def z = position.z
36
+
37
+ def to_s
38
+ "Death on day #{day} at #{position}"
39
+ end
40
+
41
+ def inspect
42
+ "#<DeathPosition day=#{day} position=#{position}>"
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -16,6 +16,10 @@ module Hytale
16
16
  @storage ||= ItemStorage.new(data["Storage"] || {})
17
17
  end
18
18
 
19
+ def backpack
20
+ @backpack ||= ItemStorage.new(data["Backpack"] || {})
21
+ end
22
+
19
23
  def hotbar
20
24
  @hotbar ||= ItemStorage.new(data["HotBar"] || {})
21
25
  end
@@ -35,8 +39,12 @@ module Hytale
35
39
  def active_hotbar_slot = data["ActiveHotbarSlot"]
36
40
  def sort_type = data["SortType"]
37
41
 
42
+ def backpack?
43
+ backpack&.simple?
44
+ end
45
+
38
46
  def all_items
39
- [storage, hotbar, armor, utility, tools].flat_map(&:items)
47
+ [storage, backpack, hotbar, armor, utility, tools].flat_map(&:items)
40
48
  end
41
49
 
42
50
  def to_h = data
@@ -13,6 +13,14 @@ module Hytale
13
13
  def capacity = data["Capacity"]
14
14
  def type = data["Id"]
15
15
 
16
+ def empty?
17
+ type == "Empty"
18
+ end
19
+
20
+ def simple?
21
+ type == "Simple"
22
+ end
23
+
16
24
  def items
17
25
  (data["Items"] || {}).map do |slot, item_data|
18
26
  Item.new(item_data, slot: slot.to_i)
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hytale
4
+ module Client
5
+ class Player
6
+ class RespawnPoint
7
+ attr_reader :data
8
+
9
+ def initialize(data)
10
+ @data = data
11
+ end
12
+
13
+ def name
14
+ data["Name"]
15
+ end
16
+
17
+ def position
18
+ pos = data["RespawnPosition"] || {}
19
+
20
+ Position.new(pos["X"], pos["Y"], pos["Z"])
21
+ end
22
+
23
+ def block_position
24
+ pos = data["BlockPosition"] || {}
25
+
26
+ Position.new(pos["X"], pos["Y"], pos["Z"])
27
+ end
28
+
29
+ def x = position.x
30
+ def y = position.y
31
+ def z = position.z
32
+
33
+ def to_s
34
+ "#{name} at #{position}"
35
+ end
36
+
37
+ def inspect
38
+ "#<RespawnPoint name=#{name.inspect} position=#{position}>"
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -22,12 +22,14 @@ module Hytale
22
22
  def position
23
23
  transform = components["Transform"] || {}
24
24
  pos = transform["Position"] || {}
25
+
25
26
  Position.new(pos["X"], pos["Y"], pos["Z"])
26
27
  end
27
28
 
28
29
  def rotation
29
30
  transform = components["Transform"] || {}
30
31
  rot = transform["Rotation"] || {}
32
+
31
33
  Rotation.new(rot["Pitch"], rot["Yaw"], rot["Roll"])
32
34
  end
33
35
 
@@ -57,11 +59,27 @@ module Hytale
57
59
  end
58
60
 
59
61
  def discovered_zones
60
- player_data.dig("PlayerData", "DiscoveredZones") || []
62
+ @discovered_zones ||= (player_data.dig("PlayerData", "DiscoveredZones") || []).map do |id|
63
+ Hytale::Client::Zone::Region.new(id)
64
+ end
65
+ end
66
+
67
+ def discovered_instances
68
+ @discovered_instances ||= (player_data.dig("PlayerData", "DiscoveredInstances") || []).map do |instance|
69
+ decode_binary_uuid(instance)
70
+ end.compact
61
71
  end
62
72
 
63
73
  def respawn_points
64
- player_data.dig("PlayerData", "PerWorldData", "default", "RespawnPoints") || []
74
+ @respawn_points ||= (player_data.dig("PlayerData", "PerWorldData", "default", "RespawnPoints") || []).map do |point|
75
+ RespawnPoint.new(point)
76
+ end
77
+ end
78
+
79
+ def death_positions
80
+ @death_positions ||= (player_data.dig("PlayerData", "PerWorldData", "default", "DeathPositions") || []).map do |pos|
81
+ DeathPosition.new(pos)
82
+ end
65
83
  end
66
84
 
67
85
  def memories
@@ -70,6 +88,48 @@ module Hytale
70
88
  end
71
89
  end
72
90
 
91
+ def known_recipes
92
+ player_data.dig("PlayerData", "KnownRecipes") || []
93
+ end
94
+
95
+ def unique_item_usages
96
+ components.dig("UniqueItemUsages", "UniqueItemUsed") || []
97
+ end
98
+
99
+ def head_rotation
100
+ rot = components.dig("HeadRotation", "Rotation") || {}
101
+
102
+ Rotation.new(rot["Pitch"], rot["Yaw"], rot["Roll"])
103
+ end
104
+
105
+ def flying?
106
+ player_data.dig("PlayerData", "PerWorldData", "default", "LastMovementStates", "Flying") || false
107
+ end
108
+
109
+ def first_spawn?
110
+ player_data.dig("PlayerData", "PerWorldData", "default", "FirstSpawn") || false
111
+ end
112
+
113
+ def active_objectives
114
+ @active_objectives ||= (player_data.dig("PlayerData", "ActiveObjectiveUUIDs") || []).map do |obj|
115
+ decode_binary_uuid(obj)
116
+ end.compact
117
+ end
118
+
119
+ def reputation_data
120
+ player_data.dig("PlayerData", "ReputationData") || {}
121
+ end
122
+
123
+ def saved_hotbars
124
+ @saved_hotbars ||= (player_data.dig("HotbarManager", "SavedHotbars") || []).compact.map do |hotbar|
125
+ ItemStorage.new(hotbar)
126
+ end
127
+ end
128
+
129
+ def current_hotbar_index
130
+ player_data.dig("HotbarManager", "CurrentHotbar")
131
+ end
132
+
73
133
  def skin
74
134
  @skin ||= PlayerSkin.find(uuid)
75
135
  end
@@ -94,6 +154,18 @@ module Hytale
94
154
  raise ParseError, "Failed to parse player data: #{e.message}"
95
155
  end
96
156
  end
157
+
158
+ private
159
+
160
+ def decode_binary_uuid(data)
161
+ return nil unless data.is_a?(Hash) && data["$binary"]
162
+
163
+ bytes = Base64.decode64(data["$binary"])
164
+ return nil unless bytes.length == 16
165
+
166
+ hex = bytes.unpack1("H*")
167
+ "#{hex[0, 8]}-#{hex[8, 4]}-#{hex[12, 4]}-#{hex[16, 4]}-#{hex[20, 12]}"
168
+ end
97
169
  end
98
170
  end
99
171
  end
@@ -10,7 +10,6 @@ module Hytale
10
10
  @path = path
11
11
  end
12
12
 
13
- # Display settings
14
13
  def fullscreen? = data["Fullscreen"]
15
14
  def maximized? = data["Maximized"]
16
15
  def window_width = data["WindowWidth"]
@@ -21,12 +20,10 @@ module Hytale
21
20
  def unlimited_fps? = data["UnlimitedFps"]
22
21
  def field_of_view = data["FieldOfView"]
23
22
 
24
- # Rendering settings
25
23
  def rendering
26
24
  @rendering ||= RenderingSettings.new(data["RenderingSettings"] || {})
27
25
  end
28
26
 
29
- # Input settings
30
27
  def input_bindings
31
28
  @input_bindings ||= InputBindings.new(data["InputBindings"] || {})
32
29
  end
@@ -35,22 +32,18 @@ module Hytale
35
32
  @mouse_settings ||= MouseSettings.new(data["MouseSettings"] || {})
36
33
  end
37
34
 
38
- # Audio settings
39
35
  def audio
40
36
  @audio ||= AudioSettings.new(data["AudioSettings"] || {})
41
37
  end
42
38
 
43
- # Gameplay settings
44
39
  def gameplay
45
40
  @gameplay ||= GameplaySettings.new(data["GameplaySettings"] || {})
46
41
  end
47
42
 
48
- # Builder tools settings
49
43
  def builder_tools
50
44
  @builder_tools ||= BuilderToolsSettings.new(data["BuilderToolsSettings"] || {})
51
45
  end
52
46
 
53
- # UI preferences
54
47
  def hide_hud? = data["HideHud"]
55
48
  def hide_hotbar? = data["HideHotbar"]
56
49
  def hide_compass? = data["HideCompass"]
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hytale
4
+ module Client
5
+ module Zone
6
+ class Region
7
+ class << self
8
+ def all
9
+ Locale.regions.keys.map { |id| new(id) }
10
+ end
11
+
12
+ def find(id)
13
+ new(id) if Locale.region_name(id)
14
+ end
15
+ end
16
+
17
+ attr_reader :id
18
+
19
+ def initialize(id)
20
+ @id = id
21
+ end
22
+
23
+ def name
24
+ Locale.region_name(id) || id
25
+ end
26
+
27
+ def zone
28
+ zone_id = REGION_PREFIX_TO_ZONE.find { |prefix, _| id.start_with?(prefix) }&.last
29
+
30
+ Zone.new(zone_id) if zone_id
31
+ end
32
+
33
+ def to_s = name
34
+ def inspect = "#<Zone::Region id=#{id.inspect} name=#{name.inspect}>"
35
+
36
+ def ==(other)
37
+ case other
38
+ when Region then id == other.id
39
+ when String then id == other
40
+ else false
41
+ end
42
+ end
43
+
44
+ alias eql? ==
45
+
46
+ def hash
47
+ id.hash
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hytale
4
+ module Client
5
+ module Zone
6
+ ZONE_TO_REGION_PREFIX = {
7
+ "Emerald_Wilds" => "Zone1",
8
+ "Howling_Sands" => "Zone2",
9
+ "Whisperfrost_Frontiers" => "Zone3",
10
+ "Devastated_Lands" => "Zone4",
11
+ "Oceans" => "Oceans",
12
+ }.freeze
13
+
14
+ REGION_PREFIX_TO_ZONE = ZONE_TO_REGION_PREFIX.invert.freeze
15
+
16
+ class << self
17
+ def all
18
+ Locale.zones.keys.map { |id| new(id) }
19
+ end
20
+
21
+ def find(id)
22
+ new(id) if Locale.zone_name(id)
23
+ end
24
+
25
+ def new(id)
26
+ Zone::Base.new(id)
27
+ end
28
+ end
29
+
30
+ class Base
31
+ attr_reader :id
32
+
33
+ def initialize(id)
34
+ @id = id
35
+ end
36
+
37
+ def name
38
+ Locale.zone_name(id) || id
39
+ end
40
+
41
+ def regions
42
+ prefix = ZONE_TO_REGION_PREFIX[id]
43
+
44
+ return [] unless prefix
45
+
46
+ Region.all.select { |region| region.id.start_with?(prefix) }
47
+ end
48
+
49
+ def to_s = name
50
+ def inspect = "#<Zone id=#{id.inspect} name=#{name.inspect}>"
51
+
52
+ def ==(other)
53
+ case other
54
+ when Base then id == other.id
55
+ when String then id == other
56
+ else false
57
+ end
58
+ end
59
+
60
+ alias eql? ==
61
+
62
+ def hash
63
+ id.hash
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
@@ -3,6 +3,9 @@
3
3
  module Hytale
4
4
  module Server
5
5
  class Process
6
+ def self.running?
7
+ false
8
+ end
6
9
  end
7
10
  end
8
11
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Hytale
4
- VERSION = "0.1.0"
4
+ VERSION = "0.1.2"
5
5
  end
data/lib/hytale.rb CHANGED
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "base64"
3
4
  require "json"
4
5
  require "time"
5
6
  require "zeitwerk"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: hytale
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.1.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Marco Roth
@@ -9,6 +9,20 @@ bindir: exe
9
9
  cert_chain: []
10
10
  date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: base64
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '0.3'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '0.3'
12
26
  - !ruby/object:Gem::Dependency
13
27
  name: chunky_png
14
28
  requirement: !ruby/object:Gem::Requirement
@@ -98,12 +112,14 @@ files:
98
112
  - lib/hytale/client/npc_memory.rb
99
113
  - lib/hytale/client/permissions.rb
100
114
  - lib/hytale/client/player.rb
115
+ - lib/hytale/client/player/death_position.rb
101
116
  - lib/hytale/client/player/entity_stats.rb
102
117
  - lib/hytale/client/player/inventory.rb
103
118
  - lib/hytale/client/player/item.rb
104
119
  - lib/hytale/client/player/item_storage.rb
105
120
  - lib/hytale/client/player/player_memory.rb
106
121
  - lib/hytale/client/player/position.rb
122
+ - lib/hytale/client/player/respawn_point.rb
107
123
  - lib/hytale/client/player/rotation.rb
108
124
  - lib/hytale/client/player/vector3.rb
109
125
  - lib/hytale/client/player_skin.rb
@@ -123,6 +139,8 @@ files:
123
139
  - lib/hytale/client/world.rb
124
140
  - lib/hytale/client/world/client_effects.rb
125
141
  - lib/hytale/client/world/death_settings.rb
142
+ - lib/hytale/client/zone.rb
143
+ - lib/hytale/client/zone/region.rb
126
144
  - lib/hytale/server.rb
127
145
  - lib/hytale/server/process.rb
128
146
  - lib/hytale/version.rb
@@ -150,5 +168,5 @@ required_rubygems_version: !ruby/object:Gem::Requirement
150
168
  requirements: []
151
169
  rubygems_version: 4.0.3
152
170
  specification_version: 4
153
- summary: Ruby bindings for reading Hytale game data
171
+ summary: Ruby gem for reading Hytale game data
154
172
  test_files: []