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
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hytale
4
+ module Client
5
+ class Memories
6
+ attr_reader :data, :path
7
+
8
+ def initialize(data, path: nil)
9
+ @data = data
10
+ @path = path
11
+ end
12
+
13
+ def all
14
+ @all ||= (data["Memories"] || []).map { |mem| NPCMemory.new(mem) }
15
+ end
16
+
17
+ def count
18
+ all.size
19
+ end
20
+
21
+ def find_by_role(role)
22
+ all.find { |mem| mem.npc_role == role }
23
+ end
24
+
25
+ def find_all_by_location(location)
26
+ all.select { |mem| mem.location&.include?(location) }
27
+ end
28
+
29
+ def roles
30
+ all.map(&:npc_role).uniq.sort
31
+ end
32
+
33
+ def locations
34
+ all.map(&:location).compact.uniq.sort
35
+ end
36
+
37
+ def each(&)
38
+ all.each(&)
39
+ end
40
+
41
+ include Enumerable
42
+
43
+ def to_a = all
44
+ def to_h = data
45
+
46
+ class << self
47
+ def load(path)
48
+ raise NotFoundError, "Memories file not found: #{path}" unless File.exist?(path)
49
+
50
+ json = File.read(path)
51
+ data = JSON.parse(json)
52
+ new(data, path: path)
53
+ rescue JSON::ParserError => e
54
+ raise ParseError, "Failed to parse memories: #{e.message}"
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hytale
4
+ module Client
5
+ class NPCMemory
6
+ attr_reader :data
7
+
8
+ def initialize(data)
9
+ @data = data
10
+ end
11
+
12
+ def id = data["Id"]
13
+ def npc_role = data["NPCRole"]
14
+ def translation_key = data["TranslationKey"]
15
+ def name_overridden? = data["IsMemoriesNameOverridden"]
16
+
17
+ def captured_at
18
+ timestamp = data["CapturedTimestamp"]
19
+ timestamp ? Time.at(timestamp / 1000.0) : nil
20
+ end
21
+
22
+ def location_key = data["FoundLocationNameKey"]
23
+
24
+ def location
25
+ location_key&.split(".")&.last
26
+ end
27
+
28
+ def friendly_name
29
+ npc_role&.gsub("_", " ")
30
+ end
31
+
32
+ def to_s
33
+ "#{friendly_name} found at #{location}"
34
+ end
35
+
36
+ def to_h = data
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hytale
4
+ module Client
5
+ class Permissions
6
+ attr_reader :data, :path
7
+
8
+ def initialize(data, path: nil)
9
+ @data = data
10
+ @path = path
11
+ end
12
+
13
+ def users
14
+ data["users"] || {}
15
+ end
16
+
17
+ def groups
18
+ data["groups"] || {}
19
+ end
20
+
21
+ def user_groups(uuid)
22
+ users.dig(uuid, "groups") || []
23
+ end
24
+
25
+ def group_permissions(group_name)
26
+ groups[group_name] || []
27
+ end
28
+
29
+ def user_permissions(uuid)
30
+ user_groups(uuid).flat_map { |g| group_permissions(g) }.uniq
31
+ end
32
+
33
+ def op?(uuid)
34
+ user_groups(uuid).include?("OP") || user_permissions(uuid).include?("*")
35
+ end
36
+
37
+ def to_h = data
38
+
39
+ class << self
40
+ def load(path)
41
+ raise NotFoundError, "Permissions file not found: #{path}" unless File.exist?(path)
42
+
43
+ json = File.read(path)
44
+ data = JSON.parse(json)
45
+ new(data, path: path)
46
+ rescue JSON::ParserError => e
47
+ raise ParseError, "Failed to parse permissions: #{e.message}"
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hytale
4
+ module Client
5
+ class Player
6
+ class EntityStats
7
+ attr_reader :data
8
+
9
+ def initialize(data)
10
+ @data = data
11
+ end
12
+
13
+ def version = data["Version"]
14
+
15
+ def health
16
+ stat("Health")
17
+ end
18
+
19
+ def max_health
20
+ base = health || 0
21
+ modifier = stat_modifier("Health", "Armor_ADDITIVE") || 0
22
+
23
+ base + modifier
24
+ end
25
+
26
+ def stamina = stat("Stamina")
27
+ def oxygen = stat("Oxygen")
28
+ def mana = stat("Mana")
29
+
30
+ def stat(name)
31
+ data.dig("Stats", name, "Value")
32
+ end
33
+
34
+ def stat_modifier(stat_name, modifier_name)
35
+ data.dig("Stats", stat_name, "Modifiers", modifier_name, "Amount")
36
+ end
37
+
38
+ def all_stats
39
+ (data["Stats"] || {}).transform_values { |v| v["Value"] }
40
+ end
41
+
42
+ def to_h = data
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hytale
4
+ module Client
5
+ class Player
6
+ class Inventory
7
+ attr_reader :data
8
+
9
+ def initialize(data)
10
+ @data = data
11
+ end
12
+
13
+ def version = data["Version"]
14
+
15
+ def storage
16
+ @storage ||= ItemStorage.new(data["Storage"] || {})
17
+ end
18
+
19
+ def backpack
20
+ @backpack ||= ItemStorage.new(data["Backpack"] || {})
21
+ end
22
+
23
+ def hotbar
24
+ @hotbar ||= ItemStorage.new(data["HotBar"] || {})
25
+ end
26
+
27
+ def armor
28
+ @armor ||= ItemStorage.new(data["Armor"] || {})
29
+ end
30
+
31
+ def utility
32
+ @utility ||= ItemStorage.new(data["Utility"] || {})
33
+ end
34
+
35
+ def tools
36
+ @tools ||= ItemStorage.new(data["Tool"] || {})
37
+ end
38
+
39
+ def active_hotbar_slot = data["ActiveHotbarSlot"]
40
+ def sort_type = data["SortType"]
41
+
42
+ def backpack?
43
+ backpack&.simple?
44
+ end
45
+
46
+ def all_items
47
+ [storage, backpack, hotbar, armor, utility, tools].flat_map(&:items)
48
+ end
49
+
50
+ def to_h = data
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,102 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hytale
4
+ module Client
5
+ class Player
6
+ class Item
7
+ attr_reader :data, :slot
8
+
9
+ def initialize(data, slot: nil)
10
+ @data = data
11
+ @slot = slot
12
+ end
13
+
14
+ def id = data["Id"]
15
+ def name = id&.gsub("_", " ")
16
+ def type = ItemType.find(id) || ItemType.new(id)
17
+ def quantity = data["Quantity"]
18
+ def durability = data["Durability"]
19
+ def max_durability = data["MaxDurability"]
20
+
21
+ def durability_percent
22
+ return nil unless durability && max_durability&.positive?
23
+
24
+ (durability / max_durability * 100).round(1)
25
+ end
26
+
27
+ def damaged?
28
+ durability && max_durability && durability < max_durability
29
+ end
30
+
31
+ def icon_path
32
+ find_icon_path || Assets.item_icon_path(id)
33
+ end
34
+
35
+ def icon_exists?
36
+ !!find_icon_path
37
+ end
38
+
39
+ private
40
+
41
+ def find_icon_path
42
+ path = Assets.item_icon_path(id)
43
+ return path if File.exist?(path)
44
+
45
+ variations = generate_icon_variations(id)
46
+
47
+ variations.each do |variation|
48
+ path = Assets.item_icon_path(variation)
49
+ return path if File.exist?(path)
50
+ end
51
+
52
+ alternate_paths = generate_alternate_paths(id)
53
+
54
+ alternate_paths.each do |alt_path|
55
+ full_path = Assets.cached_path(alt_path)
56
+ return full_path if File.exist?(full_path)
57
+ end
58
+
59
+ nil
60
+ end
61
+
62
+ def generate_icon_variations(item_id)
63
+ variations = []
64
+
65
+ variations << item_id.gsub("Shortbow", "Bow") if item_id.include?("Shortbow")
66
+
67
+ variations << item_id.gsub("Longbow", "Bow") if item_id.include?("Longbow")
68
+
69
+ variations << item_id.gsub("Longsword", "Sword") if item_id.include?("Longsword")
70
+
71
+ variations
72
+ end
73
+
74
+ def generate_alternate_paths(item_id)
75
+ paths = []
76
+
77
+ # EditorTool_Paint -> Common/Icons/Items/EditorTools/Paint.png
78
+ if item_id.start_with?("EditorTool_")
79
+ tool_name = item_id.sub("EditorTool_", "")
80
+
81
+ paths << "Common/Icons/Items/EditorTools/#{tool_name}.png"
82
+ end
83
+
84
+ paths
85
+ end
86
+
87
+ public
88
+
89
+ def to_s
90
+ string = name.to_s
91
+
92
+ string += " x#{quantity}" if quantity && quantity > 1
93
+ string += " (#{durability_percent}%)" if durability_percent && durability_percent < 100
94
+
95
+ string
96
+ end
97
+
98
+ def to_h = data
99
+ end
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hytale
4
+ module Client
5
+ class Player
6
+ class ItemStorage
7
+ attr_reader :data
8
+
9
+ def initialize(data)
10
+ @data = data
11
+ end
12
+
13
+ def capacity = data["Capacity"]
14
+ def type = data["Id"]
15
+
16
+ def empty?
17
+ type == "Empty"
18
+ end
19
+
20
+ def simple?
21
+ type == "Simple"
22
+ end
23
+
24
+ def items
25
+ (data["Items"] || {}).map do |slot, item_data|
26
+ Item.new(item_data, slot: slot.to_i)
27
+ end
28
+ end
29
+
30
+ def [](slot)
31
+ item_data = data.dig("Items", slot.to_s)
32
+
33
+ Item.new(item_data, slot: slot) if item_data
34
+ end
35
+
36
+ def to_h = data
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hytale
4
+ module Client
5
+ class Player
6
+ class PlayerMemory
7
+ attr_reader :data
8
+
9
+ def initialize(data)
10
+ @data = data
11
+ end
12
+
13
+ def id = data["Id"]
14
+ def npc_role = data["NPCRole"]
15
+ def translation_key = data["TranslationKey"]
16
+ def name_overridden? = data["IsMemoriesNameOverridden"]
17
+ def captured_at = data["CapturedTimestamp"] ? Time.at(data["CapturedTimestamp"] / 1000.0) : nil
18
+ def location_key = data["FoundLocationNameKey"]
19
+
20
+ def location
21
+ location_key&.split(".")&.last
22
+ end
23
+
24
+ def to_s = "#{npc_role} (#{location})"
25
+ def to_h = data
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hytale
4
+ module Client
5
+ class Player
6
+ Position = Data.define(:x, :y, :z) do
7
+ def to_s = "(#{x&.round(2)}, #{y&.round(2)}, #{z&.round(2)})"
8
+ def to_a = [x, y, z]
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hytale
4
+ module Client
5
+ class Player
6
+ Rotation = Data.define(:pitch, :yaw, :roll) do
7
+ def to_s = "(pitch: #{pitch&.round(2)}, yaw: #{yaw&.round(2)}, roll: #{roll&.round(2)})"
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hytale
4
+ module Client
5
+ class Player
6
+ Vector3 = Data.define(:x, :y, :z) do
7
+ def to_s = "(#{x&.round(4)}, #{y&.round(4)}, #{z&.round(4)})"
8
+ def magnitude = Math.sqrt(((x || 0)**2) + ((y || 0)**2) + ((z || 0)**2))
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,101 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hytale
4
+ module Client
5
+ class Player
6
+ attr_reader :data, :uuid, :path
7
+
8
+ def initialize(data, uuid:, path: nil)
9
+ @data = data
10
+ @uuid = uuid
11
+ @path = path
12
+ end
13
+
14
+ def components
15
+ data["Components"] || {}
16
+ end
17
+
18
+ def name
19
+ components.dig("Nameplate", "Text") || components.dig("DisplayName", "DisplayName", "RawText")
20
+ end
21
+
22
+ def position
23
+ transform = components["Transform"] || {}
24
+ pos = transform["Position"] || {}
25
+ Position.new(pos["X"], pos["Y"], pos["Z"])
26
+ end
27
+
28
+ def rotation
29
+ transform = components["Transform"] || {}
30
+ rot = transform["Rotation"] || {}
31
+ Rotation.new(rot["Pitch"], rot["Yaw"], rot["Roll"])
32
+ end
33
+
34
+ def velocity
35
+ vel = components.dig("Velocity", "Velocity") || {}
36
+ Vector3.new(vel["X"], vel["Y"], vel["Z"])
37
+ end
38
+
39
+ def stats
40
+ @stats ||= EntityStats.new(components["EntityStats"] || {})
41
+ end
42
+
43
+ def inventory
44
+ @inventory ||= Inventory.new(components.dig("Player", "Inventory") || {})
45
+ end
46
+
47
+ def player_data
48
+ components["Player"] || {}
49
+ end
50
+
51
+ def game_mode
52
+ player_data["GameMode"]
53
+ end
54
+
55
+ def current_world
56
+ player_data.dig("PlayerData", "World")
57
+ end
58
+
59
+ def discovered_zones
60
+ @discovered_zones ||= (player_data.dig("PlayerData", "DiscoveredZones") || []).map do |id|
61
+ Hytale::Client::Zone::Region.new(id)
62
+ end
63
+ end
64
+
65
+ def respawn_points
66
+ player_data.dig("PlayerData", "PerWorldData", "default", "RespawnPoints") || []
67
+ end
68
+
69
+ def memories
70
+ @memories ||= (components.dig("PlayerMemories", "Memories") || []).map do |mem|
71
+ PlayerMemory.new(mem)
72
+ end
73
+ end
74
+
75
+ def skin
76
+ @skin ||= PlayerSkin.find(uuid)
77
+ end
78
+
79
+ def avatar_preview_path
80
+ skin&.avatar_preview_path
81
+ end
82
+
83
+ def to_h
84
+ data
85
+ end
86
+
87
+ class << self
88
+ def load(path)
89
+ raise NotFoundError, "Player file not found: #{path}" unless File.exist?(path)
90
+
91
+ uuid = File.basename(path, ".json")
92
+ json = File.read(path)
93
+ data = JSON.parse(json)
94
+ new(data, uuid: uuid, path: path)
95
+ rescue JSON::ParserError => e
96
+ raise ParseError, "Failed to parse player data: #{e.message}"
97
+ end
98
+ end
99
+ end
100
+ end
101
+ end