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.
- checksums.yaml +4 -4
- data/LICENSE.txt +21 -0
- data/README.md +1430 -15
- data/exe/hytale +497 -0
- data/lib/hytale/client/assets.rb +207 -0
- data/lib/hytale/client/block_type.rb +169 -0
- data/lib/hytale/client/config.rb +98 -0
- data/lib/hytale/client/cosmetics.rb +95 -0
- data/lib/hytale/client/item_type.rb +248 -0
- data/lib/hytale/client/launcher_log/launcher_log_entry.rb +58 -0
- data/lib/hytale/client/launcher_log/launcher_log_session.rb +57 -0
- data/lib/hytale/client/launcher_log.rb +99 -0
- data/lib/hytale/client/locale.rb +234 -0
- data/lib/hytale/client/map/block.rb +135 -0
- data/lib/hytale/client/map/chunk.rb +695 -0
- data/lib/hytale/client/map/marker.rb +50 -0
- data/lib/hytale/client/map/region.rb +278 -0
- data/lib/hytale/client/map/renderer.rb +435 -0
- data/lib/hytale/client/map.rb +271 -0
- data/lib/hytale/client/memories.rb +59 -0
- data/lib/hytale/client/npc_memory.rb +39 -0
- data/lib/hytale/client/permissions.rb +52 -0
- data/lib/hytale/client/player/entity_stats.rb +46 -0
- data/lib/hytale/client/player/inventory.rb +54 -0
- data/lib/hytale/client/player/item.rb +102 -0
- data/lib/hytale/client/player/item_storage.rb +40 -0
- data/lib/hytale/client/player/player_memory.rb +29 -0
- data/lib/hytale/client/player/position.rb +12 -0
- data/lib/hytale/client/player/rotation.rb +11 -0
- data/lib/hytale/client/player/vector3.rb +12 -0
- data/lib/hytale/client/player.rb +101 -0
- data/lib/hytale/client/player_skin.rb +179 -0
- data/lib/hytale/client/prefab/palette_entry.rb +49 -0
- data/lib/hytale/client/prefab.rb +184 -0
- data/lib/hytale/client/process.rb +57 -0
- data/lib/hytale/client/save/backup.rb +43 -0
- data/lib/hytale/client/save/server_log.rb +42 -0
- data/lib/hytale/client/save.rb +157 -0
- data/lib/hytale/client/settings/audio_settings.rb +30 -0
- data/lib/hytale/client/settings/builder_tools_settings.rb +27 -0
- data/lib/hytale/client/settings/gameplay_settings.rb +23 -0
- data/lib/hytale/client/settings/input_bindings.rb +25 -0
- data/lib/hytale/client/settings/mouse_settings.rb +23 -0
- data/lib/hytale/client/settings/rendering_settings.rb +30 -0
- data/lib/hytale/client/settings.rb +74 -0
- data/lib/hytale/client/world/client_effects.rb +24 -0
- data/lib/hytale/client/world/death_settings.rb +22 -0
- data/lib/hytale/client/world.rb +88 -0
- data/lib/hytale/client/zone/region.rb +52 -0
- data/lib/hytale/client/zone.rb +68 -0
- data/lib/hytale/client.rb +142 -0
- data/lib/hytale/server/process.rb +11 -0
- data/lib/hytale/server.rb +6 -0
- data/lib/hytale/version.rb +1 -1
- data/lib/hytale.rb +37 -2
- 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
|