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,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
|