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,169 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hytale
4
+ module Client
5
+ # Represents a type of block (e.g., "Rock_Stone", "Soil_Grass")
6
+ # This is the definition/template of a block type with texture and category info.
7
+ # For positioned blocks in the world, see Block.
8
+ class BlockType
9
+ attr_reader :id
10
+
11
+ def initialize(id)
12
+ @id = id
13
+ end
14
+
15
+ def category
16
+ parts = id.split("_")
17
+
18
+ parts.first if parts.any?
19
+ end
20
+
21
+ def subcategory
22
+ parts = id.split("_")
23
+
24
+ parts[1] if parts.length > 2
25
+ end
26
+
27
+ def name
28
+ id.tr("_", " ")
29
+ end
30
+
31
+ def state_definition?
32
+ id.start_with?("*")
33
+ end
34
+
35
+ def base_id
36
+ state_definition? ? id[1..] : id
37
+ end
38
+
39
+ def texture_name
40
+ "#{base_id}.png"
41
+ end
42
+
43
+ def texture_path
44
+ find_texture_path || Assets.block_texture_path(texture_name)
45
+ end
46
+
47
+ def texture_exists?
48
+ !!find_texture_path
49
+ end
50
+
51
+ def texture_data
52
+ path = find_texture_path
53
+
54
+ return nil unless path
55
+
56
+ relative_path = path.sub("#{Assets.cache_path}/", "")
57
+
58
+ Assets.read(relative_path)
59
+ end
60
+
61
+ private
62
+
63
+ def find_texture_path
64
+ path = Assets.block_texture_path(texture_name)
65
+
66
+ return path if File.exist?(path)
67
+
68
+ variants = [
69
+ "#{base_id}_Sunny",
70
+ "#{base_id}_Deep",
71
+ "#{base_id}_Top",
72
+ "#{base_id}_Side",
73
+ base_id,
74
+ ]
75
+
76
+ variants.each do |variant|
77
+ path = Assets.block_texture_path("#{variant}.png")
78
+ return path if File.exist?(path)
79
+ end
80
+
81
+ nil
82
+ end
83
+
84
+ public
85
+
86
+ def to_s
87
+ "BlockType: #{name}"
88
+ end
89
+
90
+ def inspect
91
+ "#<Hytale::Client::BlockType id=#{id.inspect}>"
92
+ end
93
+
94
+ def to_h
95
+ {
96
+ id: id,
97
+ category: category,
98
+ name: name,
99
+ texture_path: texture_path,
100
+ }
101
+ end
102
+
103
+ def ==(other)
104
+ other.is_a?(BlockType) && other.id == id
105
+ end
106
+
107
+ def eql?(other)
108
+ self == other
109
+ end
110
+
111
+ def hash
112
+ id.hash
113
+ end
114
+
115
+ class << self
116
+ def all
117
+ @all ||= load_all_blocks
118
+ end
119
+
120
+ def find(id)
121
+ all.find { |block| block.id == id }
122
+ end
123
+
124
+ def where(category: nil, subcategory: nil)
125
+ results = all
126
+ results = results.select { |b| b.category == category } if category
127
+ results = results.select { |b| b.subcategory == subcategory } if subcategory
128
+
129
+ results
130
+ end
131
+
132
+ def categories
133
+ all.map(&:category).compact.uniq.sort
134
+ end
135
+
136
+ def subcategories
137
+ all.map(&:subcategory).compact.uniq.sort
138
+ end
139
+
140
+ def count
141
+ all.count
142
+ end
143
+
144
+ def reload!
145
+ @all = nil
146
+ end
147
+
148
+ def all_textures
149
+ Assets.block_textures
150
+ end
151
+
152
+ private
153
+
154
+ def load_all_blocks
155
+ texture_names = Assets.block_textures
156
+
157
+ block_ids = texture_names
158
+ .reject { |t| t.start_with?("T_") } # Helper textures (T_Crack_*, etc.)
159
+ .reject { |t| t.start_with?("_") } # Internal textures
160
+ .reject { |t| t.end_with?("_GS") } # Greyscale variants
161
+ .uniq
162
+ .sort
163
+
164
+ block_ids.map { |id| new(id) }
165
+ end
166
+ end
167
+ end
168
+ end
169
+ end
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hytale
4
+ module Client
5
+ module Config
6
+ class << self
7
+ def data_path
8
+ @data_path ||= default_data_path
9
+ end
10
+
11
+ attr_writer :data_path, :assets_cache_path
12
+
13
+ def reset!
14
+ @data_path = nil
15
+ end
16
+
17
+ def default_data_path
18
+ case RUBY_PLATFORM
19
+ when /darwin/
20
+ File.expand_path("~/Library/Application Support/Hytale")
21
+ when /mswin|mingw|cygwin/
22
+ File.join(ENV.fetch("APPDATA", ""), "Hytale")
23
+ when /linux/
24
+ File.expand_path("~/.local/share/Hytale")
25
+ else
26
+ raise Error, "Unsupported platform: #{RUBY_PLATFORM}"
27
+ end
28
+ end
29
+
30
+ def user_data_path
31
+ File.join(data_path, "UserData")
32
+ end
33
+
34
+ def settings_path
35
+ File.join(user_data_path, "Settings.json")
36
+ end
37
+
38
+ def saves_path
39
+ File.join(user_data_path, "Saves")
40
+ end
41
+
42
+ def logs_path
43
+ File.join(user_data_path, "Logs")
44
+ end
45
+
46
+ def telemetry_path
47
+ File.join(user_data_path, "Telemetry")
48
+ end
49
+
50
+ def launcher_log_path
51
+ File.join(data_path, "hytale-launcher.log")
52
+ end
53
+
54
+ def install_path
55
+ File.join(data_path, "install")
56
+ end
57
+
58
+ def prefab_cache_path
59
+ File.join(user_data_path, "PrefabCache")
60
+ end
61
+
62
+ def player_skins_path
63
+ File.join(user_data_path, "CachedPlayerSkins")
64
+ end
65
+
66
+ def avatar_previews_path
67
+ File.join(user_data_path, "CachedAvatarPreviews")
68
+ end
69
+
70
+ def prefabs_path
71
+ pattern = File.join(prefab_cache_path, "*", "Server", "Prefabs")
72
+ dirs = Dir.glob(pattern)
73
+
74
+ dirs.first
75
+ end
76
+
77
+ def assets_path
78
+ pattern = File.join(install_path, "release", "package", "game", "*", "Assets.zip")
79
+ files = Dir.glob(pattern)
80
+
81
+ files.first
82
+ end
83
+
84
+ def assets_cache_path
85
+ @assets_cache_path ||= File.expand_path("assets", gem_root)
86
+ end
87
+
88
+ def gem_root
89
+ File.expand_path("../../..", __dir__)
90
+ end
91
+
92
+ def exists?
93
+ File.directory?(data_path)
94
+ end
95
+ end
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hytale
4
+ module Client
5
+ class Cosmetics
6
+ CATALOG_PATH = "Cosmetics/CharacterCreator"
7
+
8
+ CATALOG_FILES = {
9
+ body_characteristics: "BodyCharacteristics.json",
10
+ capes: "Capes.json",
11
+ ear_accessories: "EarAccessory.json",
12
+ ears: "Ears.json",
13
+ eyebrows: "Eyebrows.json",
14
+ eyes: "Eyes.json",
15
+ face_accessories: "FaceAccessory.json",
16
+ faces: "Faces.json",
17
+ facial_hair: "FacialHair.json",
18
+ gloves: "Gloves.json",
19
+ haircuts: "Haircuts.json",
20
+ head_accessories: "HeadAccessory.json",
21
+ mouths: "Mouths.json",
22
+ overpants: "Overpants.json",
23
+ overtops: "Overtops.json",
24
+ pants: "Pants.json",
25
+ shoes: "Shoes.json",
26
+ skin_features: "SkinFeatures.json",
27
+ undertops: "Undertops.json",
28
+ underwear: "Underwear.json",
29
+ }.freeze
30
+
31
+ class << self
32
+ def catalog(type)
33
+ @catalogs ||= {}
34
+ @catalogs[type] ||= load_catalog(type)
35
+ end
36
+
37
+ # Handle format like "VikinManBun.BrownDark" - split off the color variant
38
+ def find(type, id)
39
+ base_id = id.to_s.split(".").first
40
+
41
+ catalog(type)&.find { |item| item["Id"] == base_id }
42
+ end
43
+
44
+ def texture_path(type, id)
45
+ item = find(type, id)
46
+ return nil unless item
47
+
48
+ variant_name = variant(id)
49
+
50
+ if variant_name && item["Textures"]
51
+ variant_data = item["Textures"][variant_name]
52
+
53
+ return Assets.cached_path("Common/#{variant_data["Texture"]}") if variant_data && variant_data["Texture"]
54
+ end
55
+
56
+ texture = item["GreyscaleTexture"] || item["Texture"]
57
+
58
+ return nil unless texture
59
+
60
+ Assets.cached_path("Common/#{texture}")
61
+ end
62
+
63
+ def model_path(type, id)
64
+ item = find(type, id)
65
+ return nil unless item
66
+
67
+ model = item["Model"]
68
+ return nil unless model
69
+
70
+ Assets.cached_path("Common/#{model}")
71
+ end
72
+
73
+ def variant(id)
74
+ parts = id.to_s.split(".")
75
+
76
+ parts.length > 1 ? parts[1..].join(".") : nil
77
+ end
78
+
79
+ private
80
+
81
+ def load_catalog(type)
82
+ filename = CATALOG_FILES[type]
83
+ return nil unless filename
84
+
85
+ path = Assets.cached_path("#{CATALOG_PATH}/#{filename}")
86
+ return nil unless File.exist?(path)
87
+
88
+ JSON.parse(File.read(path))
89
+ rescue JSON::ParserError
90
+ nil
91
+ end
92
+ end
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,248 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module Hytale
6
+ module Client
7
+ # Represents a type of item with its full definition from game assets
8
+ # This includes stats, recipes, quality, etc.
9
+ class ItemType
10
+ ITEMS_PATH = "Server/Item/Items"
11
+ ICONS_PATH = "Common/Icons/ItemsGenerated"
12
+
13
+ attr_reader :id
14
+
15
+ def initialize(id, data: nil)
16
+ @id = id
17
+ @data = data
18
+ end
19
+
20
+ def data
21
+ @data ||= load_definition
22
+ end
23
+
24
+ def name(locale: nil)
25
+ if locale
26
+ Locale.item_name(id, locale: locale) || id.tr("_", " ")
27
+ else
28
+ Locale.item_name(id) || id.tr("_", " ")
29
+ end
30
+ end
31
+
32
+ def description(locale: nil)
33
+ if locale
34
+ Locale.item_description(id, locale: locale)
35
+ else
36
+ Locale.item_description(id)
37
+ end
38
+ end
39
+
40
+ def category
41
+ parts = id.split("_")
42
+
43
+ parts.first if parts.any?
44
+ end
45
+
46
+ def subcategory
47
+ parts = id.split("_")
48
+
49
+ parts[1] if parts.length > 2
50
+ end
51
+
52
+ def parent
53
+ data&.dig("Parent")
54
+ end
55
+
56
+ def quality
57
+ data&.dig("Quality")
58
+ end
59
+
60
+ def item_level
61
+ data&.dig("ItemLevel")
62
+ end
63
+
64
+ def max_durability
65
+ data&.dig("MaxDurability")
66
+ end
67
+
68
+ def max_stack_size
69
+ data&.dig("MaxStackSize")
70
+ end
71
+
72
+ def recipe
73
+ data&.dig("Recipe")
74
+ end
75
+
76
+ def recipe_inputs
77
+ recipe&.dig("Input") || []
78
+ end
79
+
80
+ def recipe_time
81
+ recipe&.dig("TimeSeconds")
82
+ end
83
+
84
+ def model_path
85
+ data&.dig("Model")
86
+ end
87
+
88
+ def texture_path
89
+ data&.dig("Texture")
90
+ end
91
+
92
+ def icon_relative_path
93
+ data&.dig("Icon")
94
+ end
95
+
96
+ def icon_path
97
+ @icon_path ||= find_icon_path
98
+ end
99
+
100
+ def icon_exists?
101
+ path = icon_path
102
+ path && File.exist?(path)
103
+ end
104
+
105
+ def definition_exists?
106
+ !!find_definition_path
107
+ end
108
+
109
+ def to_s
110
+ "ItemType: #{name}"
111
+ end
112
+
113
+ def inspect
114
+ "#<Hytale::Client::ItemType id=#{id.inspect} quality=#{quality.inspect}>"
115
+ end
116
+
117
+ def to_h
118
+ {
119
+ id: id,
120
+ name: name,
121
+ category: category,
122
+ quality: quality,
123
+ item_level: item_level,
124
+ max_durability: max_durability,
125
+ icon_path: icon_path,
126
+ }
127
+ end
128
+
129
+ def ==(other)
130
+ other.is_a?(ItemType) && other.id == id
131
+ end
132
+
133
+ def eql?(other)
134
+ self == other
135
+ end
136
+
137
+ def hash
138
+ id.hash
139
+ end
140
+
141
+ class << self
142
+ def all
143
+ @all ||= load_all_items
144
+ end
145
+
146
+ def find(id)
147
+ all.find { |item| item.id == id }
148
+ end
149
+
150
+ def where(category: nil, quality: nil)
151
+ results = all
152
+
153
+ results = results.select { |i| i.category == category } if category
154
+ results = results.select { |i| i.quality == quality } if quality
155
+
156
+ results
157
+ end
158
+
159
+ def categories
160
+ all.map(&:category).compact.uniq.sort
161
+ end
162
+
163
+ def qualities
164
+ all.map(&:quality).compact.uniq.sort
165
+ end
166
+
167
+ def count
168
+ all.count
169
+ end
170
+
171
+ def reload!
172
+ @all = nil
173
+ @definition_paths = nil
174
+ end
175
+
176
+ def definition_paths
177
+ @definition_paths ||= Assets.list(ITEMS_PATH)
178
+ .select { |f| f.end_with?(".json") }
179
+ .to_h { |f| [File.basename(f, ".json"), f] }
180
+ end
181
+
182
+ private
183
+
184
+ def load_all_items
185
+ icon_ids = Assets.item_icons
186
+ definition_ids = definition_paths.keys
187
+ all_ids = (icon_ids + definition_ids).uniq.sort
188
+
189
+ all_ids.map { |id| new(id) }
190
+ end
191
+ end
192
+
193
+ private
194
+
195
+ def find_icon_path
196
+ path = Assets.item_icon_path(id)
197
+
198
+ return path if File.exist?(path)
199
+
200
+ variations = generate_icon_variations
201
+
202
+ variations.each do |variation|
203
+ path = Assets.item_icon_path(variation)
204
+
205
+ return path if File.exist?(path)
206
+ end
207
+
208
+ if id.start_with?("EditorTool_")
209
+ tool_name = id.sub("EditorTool_", "")
210
+ path = Assets.cached_path("Common/Icons/Items/EditorTools/#{tool_name}.png")
211
+
212
+ return path if File.exist?(path)
213
+ end
214
+
215
+ nil
216
+ end
217
+
218
+ def generate_icon_variations
219
+ variations = []
220
+
221
+ variations << id.gsub("Shortbow", "Bow") if id.include?("Shortbow")
222
+ variations << id.gsub("Longbow", "Bow") if id.include?("Longbow")
223
+ variations << id.gsub("Longsword", "Sword") if id.include?("Longsword")
224
+
225
+ variations
226
+ end
227
+
228
+ def find_definition_path
229
+ self.class.definition_paths[id]
230
+ end
231
+
232
+ def load_definition
233
+ relative_path = find_definition_path
234
+
235
+ return nil unless relative_path
236
+
237
+ Assets.extract(relative_path)
238
+ full_path = Assets.cached_path(relative_path)
239
+
240
+ return nil unless File.exist?(full_path)
241
+
242
+ JSON.parse(File.read(full_path))
243
+ rescue JSON::ParserError
244
+ nil
245
+ end
246
+ end
247
+ end
248
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hytale
4
+ module Client
5
+ class LauncherLog
6
+ class LauncherLogEntry
7
+ attr_reader :timestamp, :level, :message, :raw
8
+
9
+ LOG_PATTERN = /^time=(\S+)\s+level=(\S+)\s+msg="([^"]*)"(.*)$/
10
+
11
+ def initialize(timestamp:, level:, message:, raw:)
12
+ @timestamp = timestamp
13
+ @level = level
14
+ @message = message
15
+ @raw = raw
16
+ end
17
+
18
+ def error? = level == "ERROR"
19
+ def warn? = level == "WARN"
20
+ def info? = level == "INFO"
21
+ def debug? = level == "DEBUG"
22
+
23
+ def attributes
24
+ attrs = {}
25
+
26
+ raw.scan(/(\w+)=(\S+|"[^"]*")/).each do |key, value|
27
+ next if ["time", "level", "msg"].include?(key)
28
+
29
+ attrs[key] = value.delete_prefix('"').delete_suffix('"')
30
+ end
31
+
32
+ attrs
33
+ end
34
+
35
+ def to_s
36
+ "[#{timestamp}] #{level}: #{message}"
37
+ end
38
+
39
+ class << self
40
+ def parse(line)
41
+ return nil unless (match = line.match(LOG_PATTERN))
42
+
43
+ timestamp = begin
44
+ Time.parse(match[1])
45
+ rescue StandardError
46
+ nil
47
+ end
48
+
49
+ level = match[2]
50
+ message = match[3]
51
+
52
+ new(timestamp: timestamp, level: level, message: message, raw: line)
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hytale
4
+ module Client
5
+ class LauncherLog
6
+ class LauncherLogSession
7
+ attr_reader :started_at, :entries
8
+
9
+ def initialize(started_at)
10
+ @started_at = started_at
11
+ @entries = []
12
+ end
13
+
14
+ def add_entry(entry)
15
+ @entries << entry
16
+ end
17
+
18
+ def version
19
+ start_entry = entries.find { |e| e.message&.include?("starting hytale-launcher") }
20
+
21
+ start_entry&.attributes&.[]("version")
22
+ end
23
+
24
+ def profile_uuid
25
+ profile_entry = entries.find { |e| e.message&.include?("setting current profile") }
26
+
27
+ return nil unless profile_entry
28
+
29
+ profile_entry.message&.match(/to (\S+)/)&.[](1)
30
+ end
31
+
32
+ def game_launched?
33
+ entries.any? { |e| e.message&.include?("starting game process") }
34
+ end
35
+
36
+ def errors
37
+ entries.select(&:error?)
38
+ end
39
+
40
+ def duration
41
+ return nil if entries.empty?
42
+
43
+ last_time = entries.last.timestamp
44
+ first_time = entries.first.timestamp
45
+
46
+ return nil unless last_time && first_time
47
+
48
+ last_time - first_time
49
+ end
50
+
51
+ def to_s
52
+ "Session at #{started_at} (v#{version})"
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end