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,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hytale
4
+ module Client
5
+ class Map
6
+ class Marker
7
+ attr_reader :data, :id
8
+
9
+ def initialize(data, id: nil)
10
+ @data = data
11
+ @id = id
12
+ end
13
+
14
+ def position
15
+ pos = data["Position"] || {}
16
+ Player::Position.new(pos["X"], pos["Y"], pos["Z"])
17
+ end
18
+
19
+ def x = position.x
20
+ def y = position.y
21
+ def z = position.z
22
+
23
+ def name_key
24
+ data["Name"]
25
+ end
26
+
27
+ def name
28
+ return nil unless name_key
29
+
30
+ parts = name_key.split(".")
31
+ name_part = parts[-2] || parts.last
32
+
33
+ name_part.gsub("_", " ")
34
+ end
35
+
36
+ def icon
37
+ data["Icon"]
38
+ end
39
+
40
+ def marker_id
41
+ data["MarkerId"]
42
+ end
43
+
44
+ def to_s
45
+ "#{name} at #{position}"
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,278 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+ require "tmpdir"
5
+
6
+ module Hytale
7
+ module Client
8
+ class Map
9
+ # Parses .region.bin files containing chunk data
10
+ #
11
+ # File format:
12
+ # Header (32 bytes):
13
+ # - "HytaleIndexedStorage" (20 bytes magic)
14
+ # - Version (4 bytes, BE) = 1
15
+ # - Chunk count (4 bytes, BE) = 1024 (32x32 chunks per region)
16
+ # - Index table size (4 bytes, BE) = 4096
17
+ #
18
+ # Index Table (4096 bytes):
19
+ # - 1024 entries of 4 bytes each (BE)
20
+ # - Non-zero = chunk exists at that position
21
+ #
22
+ # Data Section:
23
+ # - Chunks stored at 4096-byte aligned positions
24
+ # - Each chunk: [decompressed_size 4B BE] [compressed_size 4B BE] [ZSTD data]
25
+ #
26
+ class Region
27
+ MAGIC = "HytaleIndexedStorage"
28
+ HEADER_SIZE = 32
29
+ CHUNKS_PER_REGION = 1024 # 32x32
30
+ CHUNK_ALIGNMENT = 4096
31
+
32
+ attr_reader :path, :x, :z
33
+
34
+ def initialize(path)
35
+ @path = path
36
+ @x, @z = parse_coordinates
37
+ @data = nil
38
+ @header_parsed = false
39
+ end
40
+
41
+ def filename
42
+ File.basename(path)
43
+ end
44
+
45
+ def size
46
+ File.size(path)
47
+ end
48
+
49
+ def size_mb
50
+ (size / 1024.0 / 1024.0).round(2)
51
+ end
52
+
53
+ def modified_at
54
+ File.mtime(path)
55
+ end
56
+
57
+ def header
58
+ parse_header unless @header_parsed
59
+
60
+ {
61
+ version: @version,
62
+ chunk_count: @chunk_count,
63
+ index_table_size: @index_table_size,
64
+ data_start: @data_start,
65
+ }
66
+ end
67
+
68
+ def chunk_count
69
+ parse_header unless @header_parsed
70
+
71
+ index_table.count(&:positive?)
72
+ end
73
+
74
+ def block_types
75
+ @block_types ||= extract_block_types
76
+ end
77
+
78
+ def chunk_exists?(local_x, local_z)
79
+ idx = (local_z * 32) + local_x
80
+ return false if idx.negative? || idx >= CHUNKS_PER_REGION
81
+
82
+ parse_header unless @header_parsed
83
+
84
+ index_table[idx].positive?
85
+ end
86
+
87
+ def chunk_data(local_x, local_z)
88
+ idx = (local_z * 32) + local_x
89
+
90
+ return nil if idx.negative? || idx >= CHUNKS_PER_REGION
91
+
92
+ chunk_at_index(idx)
93
+ end
94
+
95
+ def each_chunk
96
+ return enum_for(:each_chunk) unless block_given?
97
+
98
+ index_table.each_with_index do |sector, idx|
99
+ next if sector.zero?
100
+
101
+ chunk = chunk_at_index(idx)
102
+
103
+ yield chunk if chunk
104
+ end
105
+ end
106
+
107
+ def chunk_at_index(idx)
108
+ return nil if idx.negative? || idx >= CHUNKS_PER_REGION
109
+
110
+ parse_header unless @header_parsed
111
+
112
+ sector = index_table[idx]
113
+ return nil if sector.zero?
114
+
115
+ parse_chunk_at_sector(sector, idx)
116
+ end
117
+
118
+ def chunks
119
+ @chunks ||= parse_chunks
120
+ end
121
+
122
+ def to_s
123
+ "Region (#{x}, #{z}) - #{size_mb} MB, #{chunk_count} chunks"
124
+ end
125
+
126
+ # Render region to PNG file
127
+ # @param path [String, nil] Output file path (nil = use cache path)
128
+ # @param scale [Integer] Scale factor (1 = 1 pixel per block)
129
+ # @param detailed [Boolean] If true, render each block individually (slower but accurate)
130
+ # @param shading [Boolean] If true, apply height-based shading for depth visualization
131
+ # @param cache [Boolean] If true, use cached image if available
132
+ def render_to_png(path = nil, scale: 1, detailed: false, shading: true, cache: true)
133
+ path ||= cache_path(scale: scale, detailed: detailed, shading: shading)
134
+
135
+ return path if cache && File.exist?(path)
136
+
137
+ FileUtils.mkdir_p(File.dirname(path))
138
+
139
+ renderer = Renderer.new
140
+ renderer.save_region(self, path, scale: scale, detailed: detailed, shading: shading)
141
+ end
142
+
143
+ def cache_path(scale: 1, detailed: false, shading: true)
144
+ save_name = path.split("Saves/")[1]&.split("/")&.first || "unknown"
145
+ world_name = path.split("/worlds/")[1]&.split("/")&.first || "default"
146
+
147
+ cache_dir = File.join(
148
+ Dir.tmpdir,
149
+ "hytale_cache",
150
+ save_name,
151
+ world_name,
152
+ "regions"
153
+ )
154
+
155
+ mode = detailed ? "detailed" : "fast"
156
+ shading_suffix = shading ? "" : "_noshade"
157
+ filename = "region_#{x}_#{z}_#{scale}x_#{mode}#{shading_suffix}.png"
158
+
159
+ File.join(cache_dir, filename)
160
+ end
161
+
162
+ def cached?(scale: 1, detailed: false, shading: true)
163
+ File.exist?(cache_path(scale: scale, detailed: detailed, shading: shading))
164
+ end
165
+
166
+ def clear_cache!
167
+ Dir.glob(File.join(File.dirname(cache_path), "region_#{x}_#{z}_*.png")).each do |f|
168
+ File.delete(f)
169
+ end
170
+ end
171
+
172
+ private
173
+
174
+ def data
175
+ @data ||= File.binread(path)
176
+ end
177
+
178
+ def parse_coordinates
179
+ match = filename.match(/^(-?\d+)\.(-?\d+)\.region\.bin$/)
180
+ return [0, 0] unless match
181
+
182
+ [match[1].to_i, match[2].to_i]
183
+ end
184
+
185
+ def parse_header
186
+ magic = data[0, 20]
187
+
188
+ raise ParseError, "Invalid region file magic" unless magic == MAGIC
189
+
190
+ @version = data[20, 4].unpack1("L>")
191
+ @chunk_count = data[24, 4].unpack1("L>")
192
+ @index_table_size = data[28, 4].unpack1("L>")
193
+ @data_start = HEADER_SIZE + @index_table_size
194
+ @header_parsed = true
195
+ end
196
+
197
+ def index_table
198
+ @index_table ||= begin
199
+ parse_header unless @header_parsed
200
+
201
+ table = []
202
+
203
+ CHUNKS_PER_REGION.times do |i|
204
+ offset = HEADER_SIZE + (i * 4)
205
+ table << data[offset, 4].unpack1("L>")
206
+ end
207
+
208
+ table
209
+ end
210
+ end
211
+
212
+ def parse_chunks
213
+ parse_header unless @header_parsed
214
+
215
+ result = {}
216
+
217
+ index_table.each_with_index do |sector, idx|
218
+ next if sector.zero?
219
+
220
+ chunk_data = parse_chunk_at_sector(sector, idx)
221
+ result[idx] = chunk_data if chunk_data
222
+ end
223
+
224
+ result
225
+ end
226
+
227
+ def parse_chunk_at_sector(sector, idx = nil)
228
+ # Sector is 1-based index into 4096-byte slots
229
+ # ZSTD data position = data_start + 8 + (sector - 1) * 4096
230
+ # Header (8 bytes) is immediately before ZSTD data
231
+ zstd_position = @data_start + 8 + ((sector - 1) * CHUNK_ALIGNMENT)
232
+ header_position = zstd_position - 8
233
+
234
+ return nil if zstd_position + 4 > data.size
235
+
236
+ zstd_magic = data[zstd_position, 4]
237
+ return nil unless zstd_magic == "\x28\xB5\x2F\xFD".b
238
+
239
+ # Read chunk header: [decompressed_size 4B BE] [compressed_size 4B BE]
240
+ data[header_position, 4].unpack1("L>")
241
+ compressed_size = data[header_position + 4, 4].unpack1("L>")
242
+
243
+ return nil if compressed_size.zero? || zstd_position + compressed_size > data.size
244
+
245
+ compressed_data = data[zstd_position, compressed_size]
246
+
247
+ begin
248
+ decompressed = decompress(compressed_data)
249
+
250
+ Chunk.new(decompressed, index: idx, region: self)
251
+ rescue StandardError => e
252
+ warn "Failed to decompress chunk #{idx}: #{e.message}" if $DEBUG
253
+
254
+ nil
255
+ end
256
+ end
257
+
258
+ def extract_block_types
259
+ types = Set.new
260
+
261
+ each_chunk do |chunk|
262
+ chunk.block_types.each { |t| types << t }
263
+ end
264
+
265
+ types.to_a.sort
266
+ end
267
+
268
+ def decompress(compressed_data)
269
+ require "zstd-ruby"
270
+
271
+ Zstd.decompress(compressed_data)
272
+ rescue LoadError
273
+ raise Error, "zstd-ruby gem required for region parsing: gem install zstd-ruby"
274
+ end
275
+ end
276
+ end
277
+ end
278
+ end