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,179 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hytale
4
+ module Client
5
+ class PlayerSkin
6
+ attr_reader :data, :uuid, :path
7
+
8
+ def initialize(data, uuid: nil, path: nil)
9
+ @data = data
10
+ @uuid = uuid
11
+ @path = path
12
+ end
13
+
14
+ def body_characteristic = data["bodyCharacteristic"]
15
+ def underwear = data["underwear"]
16
+ def face = data["face"]
17
+ def ears = data["ears"]
18
+ def mouth = data["mouth"]
19
+ def haircut = data["haircut"]
20
+ def facial_hair = data["facialHair"]
21
+ def eyebrows = data["eyebrows"]
22
+ def eyes = data["eyes"]
23
+ def pants = data["pants"]
24
+ def overpants = data["overpants"]
25
+ def undertop = data["undertop"]
26
+ def overtop = data["overtop"]
27
+ def shoes = data["shoes"]
28
+ def head_accessory = data["headAccessory"]
29
+ def face_accessory = data["faceAccessory"]
30
+ def ear_accessory = data["earAccessory"]
31
+ def skin_feature = data["skinFeature"]
32
+ def gloves = data["gloves"]
33
+ def cape = data["cape"]
34
+
35
+ def equipped_items
36
+ data.compact
37
+ end
38
+
39
+ def empty_slots
40
+ data.select { |_, v| v.nil? }.keys
41
+ end
42
+
43
+ def avatar_preview_path
44
+ return nil unless uuid
45
+
46
+ path = File.join(Config.avatar_previews_path, "#{uuid}.png")
47
+ File.exist?(path) ? path : nil
48
+ end
49
+
50
+ def avatar_preview_data
51
+ path = avatar_preview_path
52
+ return nil unless path
53
+
54
+ File.binread(path)
55
+ end
56
+
57
+ def haircut_texture_path
58
+ Cosmetics.texture_path(:haircuts, haircut)
59
+ end
60
+
61
+ def facial_hair_texture_path
62
+ Cosmetics.texture_path(:facial_hair, facial_hair)
63
+ end
64
+
65
+ def eyebrows_texture_path
66
+ Cosmetics.texture_path(:eyebrows, eyebrows)
67
+ end
68
+
69
+ def eyes_texture_path
70
+ Cosmetics.texture_path(:eyes, eyes)
71
+ end
72
+
73
+ def face_texture_path
74
+ Cosmetics.texture_path(:faces, face)
75
+ end
76
+
77
+ def pants_texture_path
78
+ Cosmetics.texture_path(:pants, pants)
79
+ end
80
+
81
+ def overpants_texture_path
82
+ Cosmetics.texture_path(:overpants, overpants)
83
+ end
84
+
85
+ def undertop_texture_path
86
+ Cosmetics.texture_path(:undertops, undertop)
87
+ end
88
+
89
+ def overtop_texture_path
90
+ Cosmetics.texture_path(:overtops, overtop)
91
+ end
92
+
93
+ def shoes_texture_path
94
+ Cosmetics.texture_path(:shoes, shoes)
95
+ end
96
+
97
+ def gloves_texture_path
98
+ Cosmetics.texture_path(:gloves, gloves)
99
+ end
100
+
101
+ def cape_texture_path
102
+ Cosmetics.texture_path(:capes, cape)
103
+ end
104
+
105
+ def head_accessory_texture_path
106
+ Cosmetics.texture_path(:head_accessories, head_accessory)
107
+ end
108
+
109
+ def face_accessory_texture_path
110
+ Cosmetics.texture_path(:face_accessories, face_accessory)
111
+ end
112
+
113
+ def ear_accessory_texture_path
114
+ Cosmetics.texture_path(:ear_accessories, ear_accessory)
115
+ end
116
+
117
+ def texture_paths
118
+ {
119
+ haircut: haircut_texture_path,
120
+ facial_hair: facial_hair_texture_path,
121
+ eyebrows: eyebrows_texture_path,
122
+ eyes: eyes_texture_path,
123
+ face: face_texture_path,
124
+ pants: pants_texture_path,
125
+ overpants: overpants_texture_path,
126
+ undertop: undertop_texture_path,
127
+ overtop: overtop_texture_path,
128
+ shoes: shoes_texture_path,
129
+ gloves: gloves_texture_path,
130
+ cape: cape_texture_path,
131
+ head_accessory: head_accessory_texture_path,
132
+ face_accessory: face_accessory_texture_path,
133
+ ear_accessory: ear_accessory_texture_path,
134
+ }.compact
135
+ end
136
+
137
+ def to_s
138
+ "PlayerSkin: #{uuid || "unknown"}"
139
+ end
140
+
141
+ def to_h
142
+ data
143
+ end
144
+
145
+ class << self
146
+ def load(path)
147
+ raise NotFoundError, "Player skin not found: #{path}" unless File.exist?(path)
148
+
149
+ json = File.read(path)
150
+ data = JSON.parse(json)
151
+ uuid = File.basename(path, ".json")
152
+
153
+ new(data, uuid: uuid, path: path)
154
+ rescue JSON::ParserError => e
155
+ raise ParseError, "Failed to parse player skin: #{e.message}"
156
+ end
157
+
158
+ def all
159
+ skins_path = Config.player_skins_path
160
+ return [] unless skins_path && File.directory?(skins_path)
161
+
162
+ Dir.glob(File.join(skins_path, "*.json")).map do |path|
163
+ load(path)
164
+ end
165
+ end
166
+
167
+ def find(uuid)
168
+ skins_path = Config.player_skins_path
169
+ return nil unless skins_path
170
+
171
+ path = File.join(skins_path, "#{uuid}.json")
172
+ return nil unless File.exist?(path)
173
+
174
+ load(path)
175
+ end
176
+ end
177
+ end
178
+ end
179
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hytale
4
+ module Client
5
+ class Prefab
6
+ class PaletteEntry
7
+ attr_reader :index, :name, :flags, :block_id, :extra
8
+
9
+ def initialize(index:, name:, flags:, block_id:, extra:)
10
+ @index = index
11
+ @name = name
12
+ @flags = flags
13
+ @block_id = block_id
14
+ @extra = extra
15
+ end
16
+
17
+ def state_definition?
18
+ name.start_with?("*")
19
+ end
20
+
21
+ def base_name
22
+ state_definition? ? name[1..] : name
23
+ end
24
+
25
+ def block_category
26
+ parts = name.split("_")
27
+
28
+ return parts[0] if parts.any?
29
+
30
+ nil
31
+ end
32
+
33
+ def to_s
34
+ "#{name} (ID: 0x#{format("%04X", block_id)})"
35
+ end
36
+
37
+ def to_h
38
+ {
39
+ index: index,
40
+ name: name,
41
+ flags: flags,
42
+ block_id: block_id,
43
+ extra: extra,
44
+ }
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,184 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hytale
4
+ module Client
5
+ # Parses .prefab.json.lpf files containing prefab structure data
6
+ #
7
+ # File format:
8
+ # Header (21 bytes):
9
+ # - Palette offset (2 bytes, BE) = typically 21
10
+ # - Header value (2 bytes, BE) = typically 10
11
+ # - Reserved/dimensions (10 bytes)
12
+ # - Palette count (2 bytes, BE)
13
+ # - Reserved (5 bytes)
14
+ #
15
+ # Block Palette:
16
+ # - N entries, each:
17
+ # - String length (1 byte)
18
+ # - Block name (N bytes)
19
+ # - Flags (2 bytes, BE)
20
+ # - Block ID (2 bytes, BE)
21
+ # - Extra data (1 byte)
22
+ #
23
+ # Placement Data:
24
+ # - Block placement coordinates (format varies)
25
+ #
26
+ class Prefab
27
+ HEADER_SIZE = 21
28
+
29
+ attr_reader :path
30
+
31
+ def initialize(path)
32
+ @path = path
33
+ @data = nil
34
+ @header_parsed = false
35
+ @palette = nil
36
+ end
37
+
38
+ def filename
39
+ File.basename(path)
40
+ end
41
+
42
+ def name
43
+ filename.sub(/\.prefab\.json\.lpf$/, "")
44
+ end
45
+
46
+ def size
47
+ File.size(path)
48
+ end
49
+
50
+ def size_kb
51
+ (size / 1024.0).round(2)
52
+ end
53
+
54
+ def modified_at
55
+ File.mtime(path)
56
+ end
57
+
58
+ def palette_offset
59
+ parse_header unless @header_parsed
60
+ @palette_offset
61
+ end
62
+
63
+ def palette_count
64
+ parse_header unless @header_parsed
65
+ @palette_count
66
+ end
67
+
68
+ def palette
69
+ @palette ||= parse_palette
70
+ end
71
+
72
+ def block_names
73
+ palette.map(&:name)
74
+ end
75
+
76
+ def block_ids
77
+ palette.map(&:block_id)
78
+ end
79
+
80
+ def block_by_id(id)
81
+ palette.find { |entry| entry.block_id == id }
82
+ end
83
+
84
+ def block_by_name(name)
85
+ palette.find { |entry| entry.name == name }
86
+ end
87
+
88
+ def category
89
+ parts = path.split(File::SEPARATOR)
90
+ prefabs_index = parts.index("Prefabs")
91
+
92
+ return nil unless prefabs_index && parts.length > prefabs_index + 1
93
+
94
+ parts[prefabs_index + 1]
95
+ end
96
+
97
+ def subcategory
98
+ parts = path.split(File::SEPARATOR)
99
+ prefabs_index = parts.index("Prefabs")
100
+
101
+ return nil unless prefabs_index && parts.length > prefabs_index + 2
102
+
103
+ parts[prefabs_index + 2]
104
+ end
105
+
106
+ def to_s
107
+ "Prefab: #{name} (#{palette.size} block types, #{size_kb} KB)"
108
+ end
109
+
110
+ def to_h
111
+ {
112
+ name: name,
113
+ path: path,
114
+ size: size,
115
+ category: category,
116
+ palette: palette.map(&:to_h),
117
+ }
118
+ end
119
+
120
+ class << self
121
+ def load(path)
122
+ raise NotFoundError, "Prefab not found: #{path}" unless File.exist?(path)
123
+
124
+ new(path)
125
+ end
126
+ end
127
+
128
+ private
129
+
130
+ def data
131
+ @data ||= File.binread(path)
132
+ end
133
+
134
+ def parse_header
135
+ return if @header_parsed
136
+
137
+ @palette_offset = data[0, 2].unpack1("n")
138
+ # Bytes 2-3: header value (unused)
139
+ # Bytes 4-13: reserved/dimension data
140
+ @palette_count = data[14, 2].unpack1("n")
141
+
142
+ @header_parsed = true
143
+ end
144
+
145
+ def parse_palette
146
+ parse_header unless @header_parsed
147
+
148
+ entries = []
149
+ pos = @palette_offset
150
+
151
+ @palette_count.times do |index|
152
+ break if pos >= data.size
153
+
154
+ str_len = data[pos].ord
155
+ pos += 1
156
+
157
+ break if pos + str_len + 5 > data.size
158
+
159
+ name = data[pos, str_len]
160
+ pos += str_len
161
+
162
+ flags = data[pos, 2].unpack1("n")
163
+ pos += 2
164
+
165
+ block_id = data[pos, 2].unpack1("n")
166
+ pos += 2
167
+
168
+ extra = data[pos].ord
169
+ pos += 1
170
+
171
+ entries << PaletteEntry.new(
172
+ index: index,
173
+ name: name,
174
+ flags: flags,
175
+ block_id: block_id,
176
+ extra: extra
177
+ )
178
+ end
179
+
180
+ entries
181
+ end
182
+ end
183
+ end
184
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hytale
4
+ module Client
5
+ class Process
6
+ attr_reader :pid, :started_at
7
+
8
+ def initialize(pid:, started_at: nil)
9
+ @pid = pid
10
+ @started_at = started_at
11
+ end
12
+
13
+ def running?
14
+ ::Process.kill(0, pid)
15
+ true
16
+ rescue Errno::ESRCH
17
+ false
18
+ rescue Errno::EPERM
19
+ true
20
+ end
21
+
22
+ class << self
23
+ def list
24
+ processes = []
25
+
26
+ case RUBY_PLATFORM
27
+ when /darwin/, /linux/
28
+ output = `pgrep -f "HytaleClient" 2>/dev/null`.strip
29
+
30
+ output.split("\n").each do |pid_str|
31
+ pid = pid_str.to_i
32
+ processes << new(pid: pid) if pid.positive?
33
+ end
34
+ when /mswin|mingw|cygwin/
35
+ output = `tasklist /FI "IMAGENAME eq HytaleClient.exe" /FO CSV 2>nul`.strip
36
+
37
+ output.split("\n").drop(1).each do |line|
38
+ parts = line.split(",")
39
+ pid = parts[1]&.tr('"', "")&.to_i
40
+ processes << new(pid: pid) if pid&.positive?
41
+ end
42
+ end
43
+
44
+ processes
45
+ end
46
+
47
+ def running?
48
+ list.any?
49
+ end
50
+
51
+ def find(pid)
52
+ list.find { |p| p.pid == pid }
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hytale
4
+ module Client
5
+ class Save
6
+ class Backup
7
+ attr_reader :path
8
+
9
+ def initialize(path)
10
+ @path = path
11
+ end
12
+
13
+ def filename
14
+ File.basename(path)
15
+ end
16
+
17
+ def size
18
+ File.size(path)
19
+ end
20
+
21
+ def size_mb
22
+ (size / 1024.0 / 1024.0).round(2)
23
+ end
24
+
25
+ # Parse from filename: 2026-01-13_17-36-28.zip
26
+ def created_at
27
+ match = filename.match(/(\d{4})-(\d{2})-(\d{2})_(\d{2})-(\d{2})-(\d{2})/)
28
+
29
+ return File.mtime(path) unless match
30
+
31
+ Time.new(
32
+ match[1].to_i, match[2].to_i, match[3].to_i,
33
+ match[4].to_i, match[5].to_i, match[6].to_i
34
+ )
35
+ end
36
+
37
+ def to_s
38
+ "Backup: #{filename} (#{size_mb} MB)"
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hytale
4
+ module Client
5
+ class Save
6
+ class ServerLog
7
+ attr_reader :path
8
+
9
+ def initialize(path)
10
+ @path = path
11
+ end
12
+
13
+ def filename
14
+ File.basename(path)
15
+ end
16
+
17
+ def content
18
+ File.read(path)
19
+ end
20
+
21
+ def lines
22
+ content.lines
23
+ end
24
+
25
+ def created_at
26
+ match = filename.match(/(\d{4})-(\d{2})-(\d{2})_(\d{2})-(\d{2})-(\d{2})/)
27
+
28
+ return File.mtime(path) unless match
29
+
30
+ Time.new(
31
+ match[1].to_i, match[2].to_i, match[3].to_i,
32
+ match[4].to_i, match[5].to_i, match[6].to_i
33
+ )
34
+ end
35
+
36
+ def to_s
37
+ "ServerLog: #{filename}"
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,157 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hytale
4
+ module Client
5
+ class Save
6
+ attr_reader :name, :path
7
+
8
+ def initialize(name, path: nil)
9
+ @name = name
10
+ @path = path || File.join(Config.saves_path, name)
11
+ end
12
+
13
+ def exists?
14
+ File.directory?(path)
15
+ end
16
+
17
+ def world(name = "default")
18
+ world_path = File.join(path, "universe", "worlds", name, "config.json")
19
+ World.load(world_path)
20
+ end
21
+
22
+ def map(world_name = "default")
23
+ Map.new(path, world_name: world_name)
24
+ end
25
+
26
+ def worlds
27
+ worlds_path = File.join(path, "universe", "worlds")
28
+
29
+ return [] unless File.directory?(worlds_path)
30
+
31
+ Dir.children(worlds_path)
32
+ .select { |d| File.directory?(File.join(worlds_path, d)) }
33
+ .map { |name| world(name) }
34
+ end
35
+
36
+ def world_names
37
+ worlds_path = File.join(path, "universe", "worlds")
38
+
39
+ return [] unless File.directory?(worlds_path)
40
+
41
+ Dir.children(worlds_path)
42
+ .select { |d| File.directory?(File.join(worlds_path, d)) }
43
+ .sort
44
+ end
45
+
46
+ def maps
47
+ world_names.map { |name| map(name) }
48
+ end
49
+
50
+ def players
51
+ players_path = File.join(path, "universe", "players")
52
+
53
+ return [] unless File.directory?(players_path)
54
+
55
+ Dir.glob(File.join(players_path, "*.json"))
56
+ .reject { |f| f.end_with?(".bak") }
57
+ .map { |f| Player.load(f) }
58
+ end
59
+
60
+ def player(uuid)
61
+ player_path = File.join(path, "universe", "players", "#{uuid}.json")
62
+ Player.load(player_path)
63
+ end
64
+
65
+ def memories
66
+ memories_path = File.join(path, "universe", "memories.json")
67
+
68
+ return Memories.new({}) unless File.exist?(memories_path)
69
+
70
+ Memories.load(memories_path)
71
+ end
72
+
73
+ def permissions
74
+ permissions_path = File.join(path, "permissions.json")
75
+
76
+ return Permissions.new({}) unless File.exist?(permissions_path)
77
+
78
+ Permissions.load(permissions_path)
79
+ end
80
+
81
+ def bans
82
+ bans_path = File.join(path, "bans.json")
83
+ return [] unless File.exist?(bans_path)
84
+
85
+ JSON.parse(File.read(bans_path))
86
+ end
87
+
88
+ def whitelist
89
+ whitelist_path = File.join(path, "whitelist.json")
90
+ return [] unless File.exist?(whitelist_path)
91
+
92
+ JSON.parse(File.read(whitelist_path))
93
+ end
94
+
95
+ def preview_path
96
+ File.join(path, "preview.png")
97
+ end
98
+
99
+ def preview_exists?
100
+ File.exist?(preview_path)
101
+ end
102
+
103
+ def backups
104
+ backup_path = File.join(path, "backup")
105
+ return [] unless File.directory?(backup_path)
106
+
107
+ Dir.glob(File.join(backup_path, "*.zip")).map do |f|
108
+ Backup.new(f)
109
+ end.sort_by(&:created_at).reverse
110
+ end
111
+
112
+ def logs
113
+ logs_path = File.join(path, "logs")
114
+ return [] unless File.directory?(logs_path)
115
+
116
+ Dir.glob(File.join(logs_path, "*.log")).map do |f|
117
+ ServerLog.new(f)
118
+ end.sort_by(&:created_at).reverse
119
+ end
120
+
121
+ def mods_path
122
+ File.join(path, "mods")
123
+ end
124
+
125
+ def mods
126
+ return [] unless File.directory?(mods_path)
127
+
128
+ Dir.children(mods_path).select { |d| File.directory?(File.join(mods_path, d)) }
129
+ end
130
+
131
+ def to_s
132
+ "Save: #{name}"
133
+ end
134
+
135
+ class << self
136
+ def all
137
+ return [] unless File.directory?(Config.saves_path)
138
+
139
+ Dir.children(Config.saves_path)
140
+ .select { |d| File.directory?(File.join(Config.saves_path, d)) }
141
+ .map { |name| new(name) }
142
+ end
143
+
144
+ def find(name)
145
+ save = new(name)
146
+ raise NotFoundError, "Save not found: #{name}" unless save.exists?
147
+
148
+ save
149
+ end
150
+
151
+ def exists?(name)
152
+ File.directory?(File.join(Config.saves_path, name))
153
+ end
154
+ end
155
+ end
156
+ end
157
+ end