1 | /* |
||
2 | * This file is part of Swf Map Loader. |
||
3 | * |
||
4 | * Swf Map Loader is free software: you can redistribute it and/or modify |
||
5 | * it under the terms of the GNU Lesser General Public License as published by |
||
6 | * the Free Software Foundation, either version 3 of the License, or |
||
7 | * (at your option) any later version. |
||
8 | * |
||
9 | * Swf Map Loader is distributed in the hope that it will be useful, |
||
10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
||
11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||
12 | * GNU Lesser General Public License for more details. |
||
13 | * |
||
14 | * You should have received a copy of the GNU Lesser General Public License |
||
15 | * along with Swf Map Loader. If not, see <https://www.gnu.org/licenses/>. |
||
16 | * |
||
17 | * Copyright (c) 2020-2020 Vincent Quatrevieux |
||
18 | */ |
||
19 | |||
20 | package fr.arakne.swfmaploader.swf; |
||
21 | |||
22 | import com.jpexs.decompiler.flash.AbortRetryIgnoreHandler; |
||
23 | import com.jpexs.decompiler.flash.SWF; |
||
24 | import com.jpexs.decompiler.flash.exporters.modes.ScriptExportMode; |
||
25 | import com.jpexs.decompiler.flash.exporters.settings.ScriptExportSettings; |
||
26 | import org.apache.commons.lang3.StringUtils; |
||
27 | |||
28 | import java.io.*; |
||
0 ignored issues
–
show
Comprehensibility
introduced
by
![]() |
|||
29 | import java.net.URL; |
||
30 | import java.nio.file.*; |
||
0 ignored issues
–
show
|
|||
31 | import java.nio.file.attribute.BasicFileAttributes; |
||
32 | import java.util.HashMap; |
||
33 | import java.util.List; |
||
34 | import java.util.Map; |
||
35 | |||
36 | /** |
||
37 | * Load and parse swf map file |
||
38 | * |
||
39 | * Note: the load method is synchronized, so it not scale on multithreading application. |
||
40 | */ |
||
41 | final public class SwfFileLoader { |
||
42 | @FunctionalInterface |
||
43 | interface Setter<T> { |
||
0 ignored issues
–
show
|
|||
44 | public void set(SwfMapStructure map, T value); |
||
0 ignored issues
–
show
|
|||
45 | } |
||
46 | |||
47 | final private String baseUrl; |
||
48 | final private String tempDir; |
||
49 | |||
50 | 1 | final private Map<String, Setter<String>> fields = new HashMap<>(); |
|
51 | |||
52 | /** |
||
53 | * @param baseUrl The base URL to load map data (must contains the protocol, host and path to maps directory) |
||
54 | * @param tempDir Directory to use for store decompiled action script files |
||
55 | */ |
||
56 | 1 | public SwfFileLoader(String baseUrl, String tempDir) { |
|
57 | 1 | this.baseUrl = baseUrl; |
|
58 | 1 | this.tempDir = tempDir; |
|
59 | |||
60 | 1 | declareFields(); |
|
61 | 1 | } |
|
62 | |||
63 | /** |
||
64 | * Load a map file from CDN |
||
65 | * |
||
66 | * @param id The map id |
||
67 | * @param version The map version string |
||
68 | * @param key The encryption key |
||
69 | * |
||
70 | * @return The loaded map structure |
||
71 | * |
||
72 | * @throws IOException When cannot load swf file |
||
73 | * @throws InterruptedException When an interruption occurs during loading the swf file |
||
74 | * @throws IllegalArgumentException When the swf file do not contains a valid map structure |
||
75 | */ |
||
76 | public SwfMapStructure load(int id, String version, String key) throws IOException, InterruptedException { |
||
0 ignored issues
–
show
|
|||
77 | 1 | SwfMapStructure structure = load(new URL(baseUrl + "/" + id + "_" + version + (key == null || key.isEmpty() ? "" : "X") + ".swf")); |
|
78 | |||
79 | 1 | structure.setVersion(version); |
|
80 | |||
81 | 1 | if (structure.id() != id) { |
|
82 | 1 | throw new IllegalArgumentException("Invalid loaded map id : expected " + id + " but get " + structure.id()); |
|
83 | } |
||
84 | |||
85 | 1 | return structure; |
|
86 | } |
||
87 | |||
88 | /** |
||
89 | * Load a map file from an URL |
||
90 | * |
||
91 | * @param mapFileUrl The file URL |
||
92 | * |
||
93 | * @return The loaded map structure |
||
94 | * |
||
95 | * @throws IOException When cannot load swf file |
||
96 | * @throws InterruptedException When an interruption occurs during loading the swf file |
||
97 | * @throws IllegalArgumentException When the swf file do not contains a valid map structure |
||
98 | */ |
||
99 | synchronized public SwfMapStructure load(URL mapFileUrl) throws IOException, InterruptedException { |
||
0 ignored issues
–
show
|
|||
100 | 1 | File outdir = new File(tempDir + File.separator + StringUtils.substringAfterLast(mapFileUrl.getPath(), "/")); |
|
101 | 1 | outdir.mkdirs(); |
|
102 | |||
103 | 1 | try (InputStream stream = mapFileUrl.openStream()) { |
|
104 | 1 | SWF swf = new SWF(stream, false); |
|
105 | |||
106 | 1 | List<File> sources = swf.exportActionScript( |
|
107 | 1 | new AbortRetryIgnoreHandler() { |
|
108 | @Override |
||
109 | public int handle(Throwable throwable) { |
||
110 | return AbortRetryIgnoreHandler.ABORT; |
||
111 | } |
||
112 | |||
113 | @Override |
||
114 | public AbortRetryIgnoreHandler getNewInstance() { |
||
115 | return this; |
||
116 | } |
||
117 | }, |
||
118 | 1 | outdir.getAbsolutePath(), |
|
119 | new ScriptExportSettings(ScriptExportMode.AS, false), |
||
120 | false, |
||
121 | null |
||
122 | ); |
||
123 | |||
124 | 1 | if (sources.size() != 1 || !"DoAction.as".equalsIgnoreCase(sources.get(0).getName())) { |
|
125 | 1 | throw new IllegalArgumentException("Invalid SWF file : missing the ActionScript file"); |
|
126 | } |
||
127 | |||
128 | 1 | return parseActionScript(new FileInputStream(sources.get(0))); |
|
129 | } finally { |
||
130 | 1 | clearTemp(outdir); |
|
131 | } |
||
132 | } |
||
133 | |||
134 | /** |
||
135 | * Parse an action script file |
||
136 | * |
||
137 | * @param stream The file stream |
||
138 | * |
||
139 | * @return The map structure |
||
140 | * |
||
141 | * @throws IOException When cannot load swf file |
||
142 | * @throws IllegalArgumentException When the swf file do not contains a valid map structure |
||
143 | */ |
||
144 | public SwfMapStructure parseActionScript(InputStream stream) throws IOException { |
||
145 | 1 | SwfMapStructure structure = new SwfMapStructure(); |
|
146 | |||
147 | 1 | try (BufferedReader reader = new BufferedReader(new InputStreamReader(stream))) { |
|
0 ignored issues
–
show
|
|||
148 | 1 | for (String line = reader.readLine(); line != null; line = reader.readLine()) { |
|
149 | 1 | parseLine(structure, line); |
|
150 | } |
||
151 | } |
||
152 | |||
153 | 1 | if (!structure.valid()) { |
|
154 | 1 | throw new IllegalArgumentException("Invalid data"); |
|
155 | } |
||
156 | |||
157 | 1 | return structure; |
|
158 | } |
||
159 | |||
160 | /** |
||
161 | * Parse a single action script line |
||
162 | * |
||
163 | * @param structure The map structure to fill |
||
164 | * @param line The line to parse |
||
165 | */ |
||
166 | private void parseLine(SwfMapStructure structure, String line) { |
||
167 | 1 | String[] parts = StringUtils.split(line, "=", 2); |
|
0 ignored issues
–
show
|
|||
168 | |||
169 | 1 | if (parts.length != 2) { |
|
0 ignored issues
–
show
|
|||
170 | 1 | return; |
|
171 | } |
||
172 | |||
173 | 1 | if (!parts[1].endsWith(";")) { |
|
174 | 1 | throw new IllegalArgumentException(); |
|
175 | } |
||
176 | |||
177 | 1 | String varName = parts[0].trim(); |
|
178 | 1 | String rawValue = StringUtils.substring(parts[1], 0, -1).trim(); |
|
0 ignored issues
–
show
|
|||
179 | |||
180 | 1 | if (!fields.containsKey(varName)) { |
|
181 | return; |
||
182 | } |
||
183 | |||
184 | 1 | fields.get(varName).set(structure, rawValue); |
|
185 | 1 | } |
|
186 | |||
187 | /** |
||
188 | * Declare file structure |
||
189 | */ |
||
190 | private void declareFields() { |
||
191 | 1 | integer("id", SwfMapStructure::setId); |
|
192 | 1 | integer("width", SwfMapStructure::setWidth); |
|
193 | 1 | integer("height", SwfMapStructure::setHeight); |
|
194 | 1 | integer("backgroundNum", SwfMapStructure::setBackgroundNum); |
|
195 | 1 | integer("ambianceId", SwfMapStructure::setAmbianceId); |
|
196 | 1 | integer("musicId", SwfMapStructure::setMusicId); |
|
197 | 1 | bool("bOutdoor", SwfMapStructure::setOutdoor); |
|
198 | 1 | integer("capabilities", SwfMapStructure::setCapabilities); |
|
199 | 1 | string("mapData", SwfMapStructure::setMapData); |
|
200 | 1 | } |
|
201 | |||
202 | /** |
||
203 | * Declare an integer field |
||
204 | * |
||
205 | * @param name Field name |
||
206 | * @param setter The setter |
||
207 | */ |
||
208 | private void integer(String name, Setter<Integer> setter) { |
||
209 | 1 | fields.put(name, (map, value) -> setter.set(map, Integer.parseInt(value))); |
|
0 ignored issues
–
show
|
|||
210 | 1 | } |
|
211 | |||
212 | /** |
||
213 | * Declare an boolean field |
||
214 | * |
||
215 | * @param name Field name |
||
216 | * @param setter The setter |
||
217 | */ |
||
218 | private void bool(String name, Setter<Boolean> setter) { |
||
219 | 1 | fields.put(name, (map, value) -> setter.set(map, "true".equals(value) || "1".equals(value))); |
|
0 ignored issues
–
show
|
|||
220 | 1 | } |
|
221 | |||
222 | /** |
||
223 | * Declare an string field |
||
224 | * |
||
225 | * @param name Field name |
||
226 | * @param setter The setter |
||
227 | */ |
||
228 | private void string(String name, Setter<String> setter) { |
||
229 | 1 | fields.put(name, (map, value) -> { |
|
0 ignored issues
–
show
|
|||
230 | 1 | if (value.charAt(0) != '"' || value.charAt(value.length() - 1) != '"') { |
|
231 | 1 | throw new IllegalArgumentException(); |
|
232 | } |
||
233 | |||
234 | 1 | setter.set(map, value.substring(1, value.length() - 1)); |
|
235 | 1 | }); |
|
236 | 1 | } |
|
237 | |||
238 | private void clearTemp(File tempdir) throws IOException { |
||
0 ignored issues
–
show
|
|||
239 | 1 | if (!tempdir.exists()) { |
|
240 | return; |
||
241 | } |
||
242 | |||
243 | 1 | Files.walkFileTree(tempdir.toPath(), new SimpleFileVisitor<Path>() { |
|
244 | @Override |
||
245 | public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException { |
||
246 | 1 | Files.delete(dir); |
|
247 | |||
248 | 1 | return FileVisitResult.CONTINUE; |
|
249 | } |
||
250 | |||
251 | @Override |
||
252 | public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { |
||
253 | 1 | Files.delete(file); |
|
254 | |||
255 | 1 | return FileVisitResult.CONTINUE; |
|
256 | } |
||
257 | }); |
||
258 | 1 | } |
|
259 | } |
||
260 |