JAL-3315 getdown_1_8_6 patch file. Needs careful applying.
[jalview.git] / getdown / src / getdown-diff-jv--1_8_6.patch
1 diff --git a/getdown/src/getdown/CHANGELOG.md b/getdown/src/getdown/CHANGELOG.md
2 index 098651eb1..ec7a923e1 100644
3 --- a/getdown/src/getdown/CHANGELOG.md
4 +++ b/getdown/src/getdown/CHANGELOG.md
5 @@ -1,16 +1,73 @@
6  # Getdown Releases
7  
8 -## 1.8.3 - Unreleased
9 +## 1.8.7 - Unreleased
10 +
11 +* Reinstated env var support in `appbase` property.
12 +
13 +* Fixed issue with `myIpAddress()` in PAC proxy support.
14 +
15 +## 1.8.6 - June 4, 2019
16 +
17 +* Fixed issues with PAC proxy support: added `myIpAddress()`, fixed `dnsResolve()`, fixed crash
18 +  when detecting PAC proxy.
19 +
20 +* Reverted env var support in `appbase` property. It's causing problems that need to be
21 +  investigated.
22 +
23 +## 1.8.5 - May 29, 2019
24 +
25 +* Fixed issues with proxy information not getting properly passed through to app.
26 +  Via [#216](//github.com/threerings/getdown/pull/216).
27 +
28 +* `appbase` and `latest` properties in `getdown.txt` now process env var subtitutions.
29 +
30 +* Added support for [Proxy Auto-config](https://en.wikipedia.org/wiki/Proxy_auto-config) via PAC
31 +  files.
32 +
33 +* Proxy handling can now recover from credentials going out of date. It will detect the error and
34 +  ask for updated credentials.
35 +
36 +* Added `try_no_proxy` system property. This instructs Getdown to always first try to run without a
37 +  proxy, regardless of whether it has been configured to use a proxy in the past. And if it can run
38 +  without a proxy, it does so for that session, but retains the proxy config for future sessions in
39 +  which the proxy may again be needed.
40 +
41 +* Added `revalidate_policy` config to control when Getdown revalidates resources (by hashing them
42 +  and comparing that hash to the values in `digest.txt`). The default, `after_update`, only
43 +  validates resources after the app is updated. A new mode, `always`, validates resources prior to
44 +  every application launch.
45 +
46 +## 1.8.4 - May 14, 2019
47 +
48 +* Added `verify_timeout` config to allow customization of the default (60 second) timeout during
49 +  the resource verification process. Apparently in some pathological situations, this is needed.
50 +  Woe betide the users who have to stare at an unmoving progress bar for more than 60 seconds.
51 +  Via [#198](//github.com/threerings/getdown/pull/198)
52 +  and [901682d](//github.com/threerings/getdown/commit/901682d).
53 +
54 +* Added `java_local_dir` config to allow custom location for Java if `java_location` is specified.
55 +  Via [#206](//github.com/threerings/getdown/pull/206).
56 +
57 +* `messages_XX.properties` files are now all maintained in UTF-8 encoding and then converted to
58 +  escaped ISO-8859-1 during the build process.
59 +
60 +* Resources and unpacked resources now support `.zip` files as well as `.jar` files.
61 +  Via [#210](//github.com/threerings/getdown/pull/210).
62 +
63 +* Fixed issue when path to JVM contained spaces. Via [#214](//github.com/threerings/getdown/pull/214).
64 +
65 +## 1.8.3 - Apr 10, 2019
66  
67  * Added support for `nresource` resources which must be jar files that contain native libraries.
68    Prior to launching the application, these resources will be unpacked and their contents added to
69    the `java.library.path` system property.
70  
71  * When the app is updated to require a new version of the JVM, that JVM will be downloaded and used
72 -  immediately during that app invocation (instead of one invocation later). Via PR#169.
73 +  immediately during that app invocation (instead of one invocation later).
74 +  Via [#169](//github.com/threerings/getdown/pull/169).
75  
76 -* When a custom JVM is installed, old JVM files will be deleted prior to unpacking the new JVM. Via
77 -  PR#170.
78 +* When a custom JVM is installed, old JVM files will be deleted prior to unpacking the new JVM.
79 +  Via [#170](//github.com/threerings/getdown/pull/170).
80  
81  * Number of concurrent downloads now defaults to num-cores minus one. Though downloads are I/O
82    bound rather than CPU bound, this still turns out to be a decent default.
83 @@ -23,6 +80,14 @@
84    credentials supplied by the user. Otherwise they will be requested every time Getdown runs, which
85    is not a viable user experience.
86  
87 +* The Getdown window can be now closed by pressing the `ESC` key.
88 +  Via [#191](//github.com/threerings/getdown/pull/191).
89 +
90 +* If no `appdir` is specified via the command line or system property, the current working
91 +  directory will be used as the `appdir`. Via [8d59367](//github.com/threerings/getdown/commit/8d59367)
92 +
93 +* A basic Russian translation has been added. Thanks [@sergiorussia](//github.com/sergiorussia)!
94 +
95  ## 1.8.2 - Nov 27, 2018
96  
97  * Fixed a data corruption bug introduced at last minute into 1.8.1 release. Oops.
98 diff --git a/getdown/src/getdown/ant/.project-MOVED b/getdown/src/getdown/ant/.project-MOVED
99 deleted file mode 100644
100 index 097cb89db..000000000
101 --- a/getdown/src/getdown/ant/.project-MOVED
102 +++ /dev/null
103 @@ -1,23 +0,0 @@
104 -<?xml version="1.0" encoding="UTF-8"?>
105 -<projectDescription>
106 -       <name>getdown-ant</name>
107 -       <comment></comment>
108 -       <projects>
109 -       </projects>
110 -       <buildSpec>
111 -               <buildCommand>
112 -                       <name>org.eclipse.jdt.core.javabuilder</name>
113 -                       <arguments>
114 -                       </arguments>
115 -               </buildCommand>
116 -               <buildCommand>
117 -                       <name>org.eclipse.m2e.core.maven2Builder</name>
118 -                       <arguments>
119 -                       </arguments>
120 -               </buildCommand>
121 -       </buildSpec>
122 -       <natures>
123 -               <nature>org.eclipse.jdt.core.javanature</nature>
124 -               <nature>org.eclipse.m2e.core.maven2Nature</nature>
125 -       </natures>
126 -</projectDescription>
127 diff --git a/getdown/src/getdown/ant/.settings/org.eclipse.core.resources.prefs b/getdown/src/getdown/ant/.settings/org.eclipse.core.resources.prefs
128 deleted file mode 100644
129 index e9441bb12..000000000
130 --- a/getdown/src/getdown/ant/.settings/org.eclipse.core.resources.prefs
131 +++ /dev/null
132 @@ -1,3 +0,0 @@
133 -eclipse.preferences.version=1
134 -encoding//src/main/java=UTF-8
135 -encoding/<project>=UTF-8
136 diff --git a/getdown/src/getdown/ant/.settings/org.eclipse.jdt.core.prefs b/getdown/src/getdown/ant/.settings/org.eclipse.jdt.core.prefs
137 deleted file mode 100644
138 index 54e56721d..000000000
139 --- a/getdown/src/getdown/ant/.settings/org.eclipse.jdt.core.prefs
140 +++ /dev/null
141 @@ -1,6 +0,0 @@
142 -eclipse.preferences.version=1
143 -org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.7
144 -org.eclipse.jdt.core.compiler.compliance=1.7
145 -org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning
146 -org.eclipse.jdt.core.compiler.release=disabled
147 -org.eclipse.jdt.core.compiler.source=1.7
148 diff --git a/getdown/src/getdown/ant/.settings/org.eclipse.m2e.core.prefs b/getdown/src/getdown/ant/.settings/org.eclipse.m2e.core.prefs
149 deleted file mode 100644
150 index f897a7f1c..000000000
151 --- a/getdown/src/getdown/ant/.settings/org.eclipse.m2e.core.prefs
152 +++ /dev/null
153 @@ -1,4 +0,0 @@
154 -activeProfiles=
155 -eclipse.preferences.version=1
156 -resolveWorkspaceProjects=true
157 -version=1
158 diff --git a/getdown/src/getdown/ant/pom.xml b/getdown/src/getdown/ant/pom.xml
159 index f8231aa2e..a72d95d87 100644
160 --- a/getdown/src/getdown/ant/pom.xml
161 +++ b/getdown/src/getdown/ant/pom.xml
162 @@ -4,7 +4,7 @@
163    <parent>
164      <groupId>com.threerings.getdown</groupId>
165      <artifactId>getdown</artifactId>
166 -    <version>1.8.3-SNAPSHOT</version>
167 +    <version>1.8.7-SNAPSHOT</version>
168    </parent>
169  
170    <artifactId>getdown-ant</artifactId>
171 diff --git a/getdown/src/getdown/ant/src/main/java/com/threerings/getdown/tools/DigesterTask.java b/getdown/src/getdown/ant/src/main/java/com/threerings/getdown/tools/DigesterTask.java
172 index 48cc8d426..76212ae89 100644
173 --- a/getdown/src/getdown/ant/src/main/java/com/threerings/getdown/tools/DigesterTask.java
174 +++ b/getdown/src/getdown/ant/src/main/java/com/threerings/getdown/tools/DigesterTask.java
175 @@ -7,16 +7,13 @@ package com.threerings.getdown.tools;
176  
177  import java.io.File;
178  import java.io.IOException;
179 -
180  import java.security.GeneralSecurityException;
181  
182  import org.apache.tools.ant.BuildException;
183  import org.apache.tools.ant.Task;
184  
185 -import com.threerings.getdown.data.Digest;
186 -
187  /**
188 - * An ant task used to create a <code>digest.txt</code> for a Getdown
189 + * An ant task used to create a {@code digest.txt} for a Getdown
190   * application deployment.
191   */
192  public class DigesterTask extends Task
193 diff --git a/getdown/src/getdown/core/.project-MOVED b/getdown/src/getdown/core/.project-MOVED
194 deleted file mode 100644
195 index 177252f5f..000000000
196 --- a/getdown/src/getdown/core/.project-MOVED
197 +++ /dev/null
198 @@ -1,23 +0,0 @@
199 -<?xml version="1.0" encoding="UTF-8"?>
200 -<projectDescription>
201 -       <name>getdown-core</name>
202 -       <comment></comment>
203 -       <projects>
204 -       </projects>
205 -       <buildSpec>
206 -               <buildCommand>
207 -                       <name>org.eclipse.jdt.core.javabuilder</name>
208 -                       <arguments>
209 -                       </arguments>
210 -               </buildCommand>
211 -               <buildCommand>
212 -                       <name>org.eclipse.m2e.core.maven2Builder</name>
213 -                       <arguments>
214 -                       </arguments>
215 -               </buildCommand>
216 -       </buildSpec>
217 -       <natures>
218 -               <nature>org.eclipse.jdt.core.javanature</nature>
219 -               <nature>org.eclipse.m2e.core.maven2Nature</nature>
220 -       </natures>
221 -</projectDescription>
222 diff --git a/getdown/src/getdown/core/.settings/org.eclipse.core.resources.prefs b/getdown/src/getdown/core/.settings/org.eclipse.core.resources.prefs
223 deleted file mode 100644
224 index 0a9bbb889..000000000
225 --- a/getdown/src/getdown/core/.settings/org.eclipse.core.resources.prefs
226 +++ /dev/null
227 @@ -1,6 +0,0 @@
228 -eclipse.preferences.version=1
229 -encoding//src/it/java=UTF-8
230 -encoding//src/main/java=UTF-8
231 -encoding//src/test/java=UTF-8
232 -encoding//src/test/resources=UTF-8
233 -encoding/<project>=UTF-8
234 diff --git a/getdown/src/getdown/core/.settings/org.eclipse.jdt.core.prefs b/getdown/src/getdown/core/.settings/org.eclipse.jdt.core.prefs
235 deleted file mode 100644
236 index 54e56721d..000000000
237 --- a/getdown/src/getdown/core/.settings/org.eclipse.jdt.core.prefs
238 +++ /dev/null
239 @@ -1,6 +0,0 @@
240 -eclipse.preferences.version=1
241 -org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.7
242 -org.eclipse.jdt.core.compiler.compliance=1.7
243 -org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning
244 -org.eclipse.jdt.core.compiler.release=disabled
245 -org.eclipse.jdt.core.compiler.source=1.7
246 diff --git a/getdown/src/getdown/core/.settings/org.eclipse.m2e.core.prefs b/getdown/src/getdown/core/.settings/org.eclipse.m2e.core.prefs
247 deleted file mode 100644
248 index f897a7f1c..000000000
249 --- a/getdown/src/getdown/core/.settings/org.eclipse.m2e.core.prefs
250 +++ /dev/null
251 @@ -1,4 +0,0 @@
252 -activeProfiles=
253 -eclipse.preferences.version=1
254 -resolveWorkspaceProjects=true
255 -version=1
256 diff --git a/getdown/src/getdown/core/pom.xml b/getdown/src/getdown/core/pom.xml
257 index efec8b6ce..5e0f852ac 100644
258 --- a/getdown/src/getdown/core/pom.xml
259 +++ b/getdown/src/getdown/core/pom.xml
260 @@ -4,7 +4,7 @@
261    <parent>
262      <groupId>com.threerings.getdown</groupId>
263      <artifactId>getdown</artifactId>
264 -    <version>1.8.3-SNAPSHOT</version>
265 +    <version>1.8.7-SNAPSHOT</version>
266    </parent>
267  
268    <artifactId>getdown-core</artifactId>
269 @@ -34,7 +34,7 @@
270         Wildcards can be used (*.mycompany.com) and multiple values can be
271         separated by commas (app1.foo.com,app2.bar.com,app3.baz.com). -->
272    <properties>
273 -    <getdown.host.whitelist>jalview.org,*.jalview.org</getdown.host.whitelist>
274 +    <getdown.host.whitelist />
275    </properties>
276  
277    <build>
278 diff --git a/getdown/src/getdown/core/src/it/java/com/threerings/getdown/tests/DigesterIT.java b/getdown/src/getdown/core/src/it/java/com/threerings/getdown/tests/DigesterIT.java
279 index 52b4b5ee3..d2ddaf272 100644
280 --- a/getdown/src/getdown/core/src/it/java/com/threerings/getdown/tests/DigesterIT.java
281 +++ b/getdown/src/getdown/core/src/it/java/com/threerings/getdown/tests/DigesterIT.java
282 @@ -5,16 +5,16 @@
283  
284  package com.threerings.getdown.tests;
285  
286 -import java.io.File;
287  import java.nio.charset.StandardCharsets;
288 -import java.nio.file.*;
289 +import java.nio.file.Files;
290 +import java.nio.file.Path;
291 +import java.nio.file.Paths;
292  import java.util.Arrays;
293  import java.util.List;
294  
295 -import org.junit.*;
296 -import static org.junit.Assert.*;
297 -
298  import com.threerings.getdown.tools.Digester;
299 +import org.junit.Test;
300 +import static org.junit.Assert.assertEquals;
301  
302  public class DigesterIT {
303  
304 @@ -32,23 +32,23 @@ public class DigesterIT {
305          Files.delete(digest2);
306  
307          assertEquals(Arrays.asList(
308 -            "getdown.txt = 779c74fb4b251e18faf6e240a0667964",
309 +            "getdown.txt = 9c9b2494929c99d44ae51034d59e1a1b",
310              "testapp.jar = 404dafa55e78b25ec0e3a936357b1883",
311              "funny%test dir/some=file.txt = d8e8fca2dc0f896fd7cb4cb0031ba249",
312              "crazyhashfile#txt = f29d23fd5ab1781bd8d0760b3a516f16",
313              "foo.jar = 46ca4cc9079d9d019bb30cd21ebbc1ec",
314              "script.sh = f66e8ea25598e67e99c47d9b0b2a2cdf",
315 -            "digest.txt = f5561d85e4d80cc85883963897e58ff6"
316 +            "digest.txt = 11f9ba349cf9edacac4d72a3158447e5"
317          ), digestLines);
318  
319          assertEquals(Arrays.asList(
320 -            "getdown.txt = 4f0c657895c3c3a35fa55bf5951c64fa9b0694f8fc685af3f1d8635c639e066b",
321 +            "getdown.txt = 1efecfae2a189002a6658f17d162b1922c7bde978944949276dc038a0df2461f",
322              "testapp.jar = c9cb1906afbf48f8654b416c3f831046bd3752a76137e5bf0a9af2f790bf48e0",
323              "funny%test dir/some=file.txt = f2ca1bb6c7e907d06dafe4687e579fce76b37e4e93b7605022da52e6ccc26fd2",
324              "crazyhashfile#txt = 6816889f922de38f145db215a28ad7c5e1badf7354b5cdab225a27486789fa3b",
325              "foo.jar = ea188b872e0496debcbe00aaadccccb12a8aa9b025bb62c130cd3d9b8540b062",
326              "script.sh = cca1c5c7628d9bf7533f655a9cfa6573d64afb8375f81960d1d832dc5135c988",
327 -            "digest2.txt = 70b442c9f56660561921da3368e1a206f05c379182fab3062750b7ddcf303407"
328 +            "digest2.txt = 41eacdabda8909bdbbf61e4f980867f4003c16a12f6770e6fc619b6af100e05b"
329          ), digest2Lines);
330      }
331  }
332 diff --git a/getdown/src/getdown/core/src/it/resources/testapp/getdown.txt b/getdown/src/getdown/core/src/it/resources/testapp/getdown.txt
333 index 3e0e5381a..ab0e47383 100644
334 --- a/getdown/src/getdown/core/src/it/resources/testapp/getdown.txt
335 +++ b/getdown/src/getdown/core/src/it/resources/testapp/getdown.txt
336 @@ -13,6 +13,18 @@ apparg = %APPDIR%
337  # test the %env% mechanism
338  jvmarg = -Dusername=\%ENV.USER%
339  
340 +# test various java_*** configs, they are not interesting for digester
341 +java_local_dir = jre
342 +java_max_version = 1089999
343 +java_min_version = [windows] 1080111
344 +java_min_version = [!windows] 1080192
345 +java_exact_version_required = [linux] true
346 +java_location = [linux-amd64] /files/java/java_linux_64.zip
347 +java_location = [linux-i386] /files/java/java_linux_32.zip
348 +java_location = [mac] /files/java/java_mac_64.zip
349 +java_location = [windows-amd64] /files/java/java_windows_64.zip
350 +java_location = [windows-x86] /files/java/java_windows_32.zip
351 +
352  strict_comments = true
353  resource = funny%test dir/some=file.txt
354  resource = crazyhashfile#txt
355 diff --git a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/Log.java b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/Log.java
356 index 13b99564a..da98c9031 100644
357 --- a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/Log.java
358 +++ b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/Log.java
359 @@ -15,7 +15,7 @@ import java.util.logging.*;
360  /**
361   * A placeholder class that contains a reference to the log object used by the Getdown code.
362   */
363 -public class Log
364 +public final class Log
365  {
366      public static class Shim {
367          /**
368 @@ -69,6 +69,13 @@ public class Log
369      /** We dispatch our log messages through this logging shim. */
370      public static final Shim log = new Shim();
371  
372 +    /**
373 +     * Formats a message with key/value pairs. The pairs will be appended to the message as a
374 +     * comma separated list of {@code key=value} in square brackets.
375 +     * @param message the main log message.
376 +     * @param args the key/value pairs. Any trailing key with no value will be ignored.
377 +     * @return the formatted message, i.e. {@code Some log message [key=value, key=value]}.
378 +     */
379      public static String format (Object message, Object... args) {
380          if (args.length < 2) return String.valueOf(message);
381          StringBuilder buf = new StringBuilder(String.valueOf(message));
382 @@ -76,13 +83,13 @@ public class Log
383              buf.append(' ');
384          }
385          buf.append('[');
386 -        for (int ii = 0; ii < args.length; ii += 2) {
387 +        for (int ii = 0, ll = args.length/2; ii < ll; ii += 1) {
388              if (ii > 0) {
389                  buf.append(',').append(' ');
390              }
391 -            buf.append(args[ii]).append('=');
392 +            buf.append(args[2*ii]).append('=');
393              try {
394 -                buf.append(args[ii+1]);
395 +                buf.append(args[2*ii+1]);
396              } catch (Throwable t) {
397                  buf.append("<toString() failure: ").append(t).append(">");
398              }
399 @@ -136,6 +143,5 @@ public class Log
400          protected FieldPosition _fpos = new FieldPosition(SimpleDateFormat.DATE_FIELD);
401      }
402  
403 -    protected static final String DATE_FORMAT = "{0,date} {0,time}";
404      protected static final Level[] LEVELS = {Level.FINE, Level.INFO, Level.WARNING, Level.SEVERE};
405  }
406 diff --git a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/cache/GarbageCollector.java b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/cache/GarbageCollector.java
407 index 67ea64575..7e01e87c5 100644
408 --- a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/cache/GarbageCollector.java
409 +++ b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/cache/GarbageCollector.java
410 @@ -6,6 +6,8 @@
411  package com.threerings.getdown.cache;
412  
413  import java.io.File;
414 +
415 +import com.threerings.getdown.data.Resource;
416  import com.threerings.getdown.util.FileUtil;
417  
418  /**
419 @@ -55,9 +57,9 @@ public class GarbageCollector
420          if (subdirs != null) {
421              for (File dir : subdirs) {
422                  if (dir.isDirectory()) {
423 -                    // Get all the native jars in the directory (there should only be one)
424 +                    // Get all the native jars or zips in the directory (there should only be one)
425                      for (File file : dir.listFiles()) {
426 -                        if (!file.getName().endsWith(".jar")) {
427 +                        if (!Resource.isJar(file) && !Resource.isZip(file)) {
428                              continue;
429                          }
430                          File cachedFile = getCachedFile(file);
431 @@ -94,6 +96,6 @@ public class GarbageCollector
432      private static File getCachedFile (File file)
433      {
434          return !isLastAccessedFile(file) ? file : new File(
435 -            file.getParentFile(), file.getName().substring(0, file.getName().lastIndexOf(".")));
436 +            file.getParentFile(), file.getName().substring(0, file.getName().lastIndexOf('.')));
437      }
438  }
439 diff --git a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/cache/ResourceCache.java b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/cache/ResourceCache.java
440 index 0210e9a86..41f0c5f6d 100644
441 --- a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/cache/ResourceCache.java
442 +++ b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/cache/ResourceCache.java
443 @@ -69,7 +69,7 @@ public class ResourceCache
444  
445      private String getFileSuffix (File fileToCache) {
446          String fileName = fileToCache.getName();
447 -        int index = fileName.lastIndexOf(".");
448 +        int index = fileName.lastIndexOf('.');
449  
450          return index > -1 ? fileName.substring(index) : "";
451      }
452 diff --git a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/data/Application.java b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/data/Application.java
453 index 0de5c8ac8..a93122553 100644
454 --- a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/data/Application.java
455 +++ b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/data/Application.java
456 @@ -22,24 +22,17 @@ import java.util.*;
457  import java.util.concurrent.*;
458  import java.util.regex.Matcher;
459  import java.util.regex.Pattern;
460 -import java.util.zip.GZIPInputStream;
461 -import com.sun.management.OperatingSystemMXBean;
462 -import java.lang.management.ManagementFactory;
463 -
464 -import jalview.bin.MemorySetting;
465  
466 +import com.threerings.getdown.net.Connector;
467  import com.threerings.getdown.util.*;
468  // avoid ambiguity with java.util.Base64 which we can't use as it's 1.8+
469  import com.threerings.getdown.util.Base64;
470  
471 -import com.threerings.getdown.data.EnvConfig;
472 -import com.threerings.getdown.data.EnvConfig.Note;
473 -
474  import static com.threerings.getdown.Log.log;
475  import static java.nio.charset.StandardCharsets.UTF_8;
476  
477  /**
478 - * Parses and provide access to the information contained in the <code>getdown.txt</code>
479 + * Parses and provide access to the information contained in the {@code getdown.txt}
480   * configuration file.
481   */
482  public class Application
483 @@ -218,10 +211,10 @@ public class Application
484       * Used by {@link #verifyMetadata} to communicate status in circumstances where it needs to
485       * take network actions.
486       */
487 -    public static interface StatusDisplay
488 +    public interface StatusDisplay
489      {
490          /** Requests that the specified status message be displayed. */
491 -        public void updateStatus (String message);
492 +        void updateStatus (String message);
493      }
494  
495      /**
496 @@ -239,19 +232,56 @@ public class Application
497          }
498      }
499  
500 -    /** The proxy that should be used to do HTTP downloads. This must be configured prior to using
501 -      * the application instance. Yes this is a public mutable field, no I'm not going to create a
502 +    /**
503 +     * Reads the {@code getdown.txt} config file into a {@code Config} object and returns it.
504 +     */
505 +    public static Config readConfig (EnvConfig envc, boolean checkPlatform) throws IOException {
506 +        Config config = null;
507 +        File cfgfile = new File(envc.appDir, CONFIG_FILE);
508 +        Config.ParseOpts opts = Config.createOpts(checkPlatform);
509 +        try {
510 +            // if we have a configuration file, read the data from it
511 +            if (cfgfile.exists()) {
512 +                config = Config.parseConfig(cfgfile, opts);
513 +            }
514 +            // otherwise, try reading data from our backup config file; thanks to funny windows
515 +            // bullshit, we have to do this backup file fiddling in case we got screwed while
516 +            // updating getdown.txt during normal operation
517 +            else if ((cfgfile = new File(envc.appDir, Application.CONFIG_FILE + "_old")).exists()) {
518 +                config = Config.parseConfig(cfgfile, opts);
519 +            }
520 +            // otherwise, issue a warning that we found no getdown file
521 +            else {
522 +                log.info("Found no getdown.txt file", "appdir", envc.appDir);
523 +            }
524 +        } catch (Exception e) {
525 +            log.warning("Failure reading config file", "file", config, e);
526 +        }
527 +
528 +        // if we failed to read our config file, check for an appbase specified via a system
529 +        // property; we can use that to bootstrap ourselves back into operation
530 +        if (config == null) {
531 +            log.info("Using 'appbase' from bootstrap config", "appbase", envc.appBase);
532 +            Map<String, Object> cdata = new HashMap<>();
533 +            cdata.put("appbase", envc.appBase);
534 +            config = new Config(cdata);
535 +        }
536 +
537 +        return config;
538 +    }
539 +
540 +    /** A helper that is used to do HTTP downloads. This must be configured prior to using the
541 +      * application instance. Yes this is a public mutable field, no I'm not going to create a
542        * getter and setter just to pretend like that's not the case. */
543 -    public Proxy proxy = Proxy.NO_PROXY;
544 +    public Connector conn = Connector.DEFAULT;
545  
546      /**
547 -     * Creates an application instance which records the location of the <code>getdown.txt</code>
548 +     * Creates an application instance which records the location of the {@code getdown.txt}
549       * configuration file from the supplied application directory.
550       *
551       */
552      public Application (EnvConfig envc) {
553          _envc = envc;
554 -        _config = getLocalPath(envc.appDir, CONFIG_FILE);
555      }
556  
557      /**
558 @@ -375,8 +405,7 @@ public class Application
559       */
560      public List<Resource> getActiveCodeResources ()
561      {
562 -        ArrayList<Resource> codes = new ArrayList<>();
563 -        codes.addAll(getCodeResources());
564 +        List<Resource> codes = new ArrayList<>(getCodeResources());
565          for (AuxGroup aux : getAuxGroups()) {
566              if (isAuxGroupActive(aux.name)) {
567                  codes.addAll(aux.codes);
568 @@ -404,8 +433,7 @@ public class Application
569       */
570      public List<Resource> getActiveResources ()
571      {
572 -        ArrayList<Resource> rsrcs = new ArrayList<>();
573 -        rsrcs.addAll(getResources());
574 +        List<Resource> rsrcs = new ArrayList<>(getResources());
575          for (AuxGroup aux : getAuxGroups()) {
576              if (isAuxGroupActive(aux.name)) {
577                  rsrcs.addAll(aux.rsrcs);
578 @@ -442,7 +470,15 @@ public class Application
579      }
580  
581      /**
582 -     * Returns a resource for a zip file containing a Java VM that can be downloaded to use in
583 +     * @return directory into which a local VM installation should be unpacked.
584 +     */
585 +    public File getJavaLocalDir ()
586 +    {
587 +        return _javaLocalDir;
588 +    }
589 +
590 +    /**
591 +     * @return a resource for a zip file containing a Java VM that can be downloaded to use in
592       * place of the installed VM (in the case where the VM that launched Getdown does not meet the
593       * application's version requirements) or null if no VM is available for this platform.
594       */
595 @@ -452,21 +488,16 @@ public class Application
596              return null;
597          }
598  
599 -        String extension = (_javaLocation.endsWith(".tgz"))?".tgz":".jar";
600 -        String vmfile = LaunchUtil.LOCAL_JAVA_DIR + extension;
601 -               log.info("vmfile is '"+vmfile+"'");
602 -               System.out.println("vmfile is '"+vmfile+"'");
603 +        // take extension from java location
604 +        String vmfileExt = _javaLocation.substring(_javaLocation.lastIndexOf('.'));
605 +        String vmfile = _javaLocalDir.getName() + vmfileExt;
606          try {
607              URL remote = new URL(createVAppBase(_targetVersion), encodePath(_javaLocation));
608 -            log.info("Attempting to fetch jvm at "+remote.toString());
609 -            System.out.println("Attempting to fetch jvm at "+remote.toString());
610              return new Resource(vmfile, remote, getLocalPath(vmfile),
611                                  EnumSet.of(Resource.Attr.UNPACK, Resource.Attr.CLEAN));
612          } catch (Exception e) {
613              log.warning("Failed to create VM resource", "vmfile", vmfile, "appbase", _appbase,
614                  "tvers", _targetVersion, "javaloc", _javaLocation, "error", e);
615 -            System.out.println("Failed to create VM resource: vmfile="+vmfile+", appbase="+_appbase+
616 -                ", tvers="+_targetVersion+", javaloc="+_javaLocation+", error="+e);
617              return null;
618          }
619      }
620 @@ -547,88 +578,52 @@ public class Application
621       * @exception IOException thrown if there is an error reading the file or an error encountered
622       * during its parsing.
623       */
624 -    public Config init (boolean checkPlatform)
625 -        throws IOException
626 +    public Config init (boolean checkPlatform) throws IOException
627      {
628 -        Config config = null;
629 -        File cfgfile = _config;
630 -        Config.ParseOpts opts = Config.createOpts(checkPlatform);
631 -        try {
632 -            // if we have a configuration file, read the data from it
633 -            if (cfgfile.exists()) {
634 -                config = Config.parseConfig(_config, opts);
635 -            }
636 -            // otherwise, try reading data from our backup config file; thanks to funny windows
637 -            // bullshit, we have to do this backup file fiddling in case we got screwed while
638 -            // updating getdown.txt during normal operation
639 -            else if ((cfgfile = getLocalPath(CONFIG_FILE + "_old")).exists()) {
640 -                config = Config.parseConfig(cfgfile, opts);
641 -            }
642 -            // otherwise, issue a warning that we found no getdown file
643 -            else {
644 -                log.info("Found no getdown.txt file", "appdir", getAppDir());
645 -            }
646 -        } catch (Exception e) {
647 -            log.warning("Failure reading config file", "file", _config, e);
648 -        }
649 -        
650 -        // see if there's an override config from locator file
651 -        Config locatorConfig = createLocatorConfig(opts);
652 -        
653 -        // merge the locator file config into config (or replace config with)
654 -        if (locatorConfig != null) {
655 -          if (config == null || locatorConfig.getBoolean(LOCATOR_FILE_EXTENSION+"_replace")) {
656 -            config = locatorConfig;
657 -          } else {
658 -            config.mergeConfig(locatorConfig, locatorConfig.getBoolean(LOCATOR_FILE_EXTENSION+"_merge"));
659 -          }
660 -        }
661 +        Config config = readConfig(_envc, checkPlatform);
662 +        initBase(config);
663 +        initJava(config);
664 +        initTracking(config);
665 +        initResources(config);
666 +        initArgs(config);
667 +        return config;
668 +    }
669  
670 -        // if we failed to read our config file, check for an appbase specified via a system
671 -        // property; we can use that to bootstrap ourselves back into operation
672 -        if (config == null) {
673 -            String appbase = _envc.appBase;
674 -            log.info("Using 'appbase' from bootstrap config", "appbase", appbase);
675 -            Map<String, Object> cdata = new HashMap<>();
676 -            cdata.put("appbase", appbase);
677 -            config = new Config(cdata);
678 -        }
679 +    /**
680 +     * Reads the basic config info from {@code config} into this instance. This includes things
681 +     * like the appbase and version.
682 +     */
683 +    public void initBase (Config config) throws IOException {
684 +        // first extract our version information
685 +        _version = config.getLong("version", -1L);
686  
687 -        // first determine our application base, this way if anything goes wrong later in the
688 +        // determine our application base, this way if anything goes wrong later in the
689          // process, our caller can use the appbase to download a new configuration file
690          _appbase = config.getString("appbase");
691 -        
692 -        // see if locatorConfig override
693 -        if (locatorConfig != null && !StringUtil.isBlank(locatorConfig.getString("appbase"))) {
694 -          _appbase = locatorConfig.getString("appbase");
695 -        }
696 -        
697          if (_appbase == null) {
698              throw new RuntimeException("m.missing_appbase");
699          }
700  
701 -        // check if we're overriding the domain in the appbase
702 -        _appbase = SysProps.overrideAppbase(_appbase);
703 +        // check if we're overriding the domain in the appbase, and sub envvars
704 +        _appbase = resolveEnvVars(SysProps.overrideAppbase(_appbase));
705  
706          // make sure there's a trailing slash
707          if (!_appbase.endsWith("/")) {
708 -            _appbase = _appbase + "/";
709 +            _appbase += "/";
710          }
711  
712 -        // extract our version information
713 -        _version = config.getLong("version", -1L);
714 -
715          // if we are a versioned deployment, create a versioned appbase
716          try {
717              _vappbase = createVAppBase(_version);
718          } catch (MalformedURLException mue) {
719              String err = MessageUtil.tcompose("m.invalid_appbase", _appbase);
720 -            throw (IOException) new IOException(err).initCause(mue);
721 +            throw new IOException(err, mue);
722          }
723  
724          // check for a latest config URL
725          String latest = config.getString("latest");
726          if (latest != null) {
727 +            latest = processArg(latest);
728              if (latest.startsWith(_appbase)) {
729                  latest = _appbase + latest.substring(_appbase.length());
730              } else {
731 @@ -641,20 +636,25 @@ public class Application
732              }
733          }
734  
735 -        String appPrefix = _envc.appId == null ? "" : (_envc.appId + ".");
736 -
737 -        // determine our application class name (use app-specific class _if_ one is provided)
738 -        _class = config.getString("class");
739 -        if (appPrefix.length() > 0) {
740 -            _class = config.getString(appPrefix + "class", _class);
741 -        }
742 -        if (_class == null) {
743 -            throw new IOException("m.missing_class");
744 -        }
745 -
746 -        // determine whether we want strict comments
747 +        // read some miscellaneous configurations
748          _strictComments = config.getBoolean("strict_comments");
749 +        _allowOffline = config.getBoolean("allow_offline");
750 +        _revalidatePolicy = config.getEnum(
751 +            "revalidate_policy", RevalidatePolicy.class, RevalidatePolicy.AFTER_UPDATE);
752 +        int tpSize = SysProps.threadPoolSize();
753 +        _maxConcDownloads = Math.max(1, config.getInt("max_concurrent_downloads", tpSize));
754 +        _verifyTimeout = config.getInt("verify_timeout", 60);
755 +
756 +        // whether to cache code resources and launch from cache
757 +        _useCodeCache = config.getBoolean("use_code_cache");
758 +        _codeCacheRetentionDays = config.getInt("code_cache_retention_days", 7);
759 +    }
760  
761 +    /**
762 +     * Reads the JVM requirements from {@code config} into this instance. This includes things like
763 +     * the min and max java version, location of a locally installed JRE, etc.
764 +     */
765 +    public void initJava (Config config) {
766          // check to see if we're using a custom java.version property and regex
767          _javaVersionProp = config.getString("java_version_prop", _javaVersionProp);
768          _javaVersionRegex = config.getString("java_version_regex", _javaVersionRegex);
769 @@ -669,14 +669,16 @@ public class Application
770          // check to see if we require a particular JVM version and have a supplied JVM
771          _javaExactVersionRequired = config.getBoolean("java_exact_version_required");
772  
773 -        // this is a little weird, but when we're run from the digester, we see a String[] which
774 -        // contains java locations for all platforms which we can't grok, but the digester doesn't
775 -        // need to know about that; when we're run in a real application there will be only one!
776 -        Object javaloc = config.getRaw("java_location");
777 -        if (javaloc instanceof String) {
778 -            _javaLocation = (String)javaloc;
779 -        }
780 +        _javaLocation = config.getString("java_location");
781 +
782 +        // used only in conjunction with java_location
783 +        _javaLocalDir = getLocalPath(config.getString("java_local_dir", LaunchUtil.LOCAL_JAVA_DIR));
784 +    }
785  
786 +    /**
787 +     * Reads the install tracking info from {@code config} into this instance.
788 +     */
789 +    public void initTracking (Config config) {
790          // determine whether we have any tracking configuration
791          _trackingURL = config.getString("tracking_url");
792  
793 @@ -701,14 +703,16 @@ public class Application
794  
795          // Some app may need to generate google analytics code
796          _trackingGAHash = config.getString("tracking_ga_hash");
797 +    }
798  
799 +    /**
800 +     * Reads the app resource info from {@code config} into this instance.
801 +     */
802 +    public void initResources (Config config) throws IOException {
803          // clear our arrays as we may be reinitializing
804          _codes.clear();
805          _resources.clear();
806          _auxgroups.clear();
807 -        _jvmargs.clear();
808 -        _appargs.clear();
809 -        _txtJvmArgs.clear();
810  
811          // parse our code resources
812          if (config.getMultiValue("code") == null &&
813 @@ -727,10 +731,10 @@ public class Application
814  
815          // parse our auxiliary resource groups
816          for (String auxgroup : config.getList("auxgroups")) {
817 -            ArrayList<Resource> codes = new ArrayList<>();
818 +            List<Resource> codes = new ArrayList<>();
819              parseResources(config, auxgroup + ".code", Resource.NORMAL, codes);
820              parseResources(config, auxgroup + ".ucode", Resource.UNPACK, codes);
821 -            ArrayList<Resource> rsrcs = new ArrayList<>();
822 +            List<Resource> rsrcs = new ArrayList<>();
823              parseResources(config, auxgroup + ".resource", Resource.NORMAL, rsrcs);
824              parseResources(config, auxgroup + ".xresource", Resource.EXEC, rsrcs);
825              parseResources(config, auxgroup + ".uresource", Resource.UNPACK, rsrcs);
826 @@ -738,87 +742,48 @@ public class Application
827              parseResources(config, auxgroup + ".nresource", Resource.NATIVE, rsrcs);
828              _auxgroups.put(auxgroup, new AuxGroup(auxgroup, codes, rsrcs));
829          }
830 +    }
831  
832 -        // transfer our JVM arguments (we include both "global" args and app_id-prefixed args)
833 -        String[] jvmargs = config.getMultiValue("jvmarg");
834 -        addAll(jvmargs, _jvmargs);
835 +    /**
836 +     * Reads the command line arg info from {@code config} into this instance.
837 +     */
838 +    public void initArgs (Config config) throws IOException {
839 +        _jvmargs.clear();
840 +        _appargs.clear();
841 +        _txtJvmArgs.clear();
842 +
843 +        String appPrefix = _envc.appId == null ? "" : (_envc.appId + ".");
844 +
845 +        // determine our application class name (use app-specific class _if_ one is provided)
846 +        _class = config.getString("class");
847          if (appPrefix.length() > 0) {
848 -            jvmargs = config.getMultiValue(appPrefix + "jvmarg");
849 -            addAll(jvmargs, _jvmargs);
850 +            _class = config.getString(appPrefix + "class", _class);
851 +        }
852 +        if (_class == null) {
853 +            throw new IOException("m.missing_class");
854          }
855  
856 -        // see if a percentage of physical memory option exists
857 -        int jvmmempc = config.getInt("jvmmempc", -1);
858 -        // app_id prefixed setting overrides
859 +        // transfer our JVM arguments (we include both "global" args and app_id-prefixed args)
860 +        addAll(config.getMultiValue("jvmarg"), _jvmargs);
861          if (appPrefix.length() > 0) {
862 -            jvmmempc = config.getInt(appPrefix + "jvmmempc", jvmmempc);
863 -        }
864 -        if (0 <= jvmmempc && jvmmempc <= 100) {
865 -          
866 -          long maxMemLong = -1;
867 -
868 -          try
869 -          {
870 -            maxMemLong = MemorySetting.memPercent(jvmmempc);
871 -          } catch (Exception e)
872 -          {
873 -            e.printStackTrace();
874 -          } catch (Throwable t)
875 -          {
876 -            t.printStackTrace();
877 -          }
878 -
879 -          if (maxMemLong > 0)
880 -          {
881 -            
882 -            String[] maxMemHeapArg = new String[]{"-Xmx"+Long.toString(maxMemLong)};
883 -            // remove other max heap size arg
884 -            ARG: for (int i = 0; i < _jvmargs.size(); i++) {
885 -              if (_jvmargs.get(i) instanceof java.lang.String && _jvmargs.get(i).startsWith("-Xmx")) {
886 -                _jvmargs.remove(i);
887 -              }
888 -            }
889 -            addAll(maxMemHeapArg, _jvmargs);
890 -            
891 -          }
892 -
893 -        } else if (jvmmempc != -1) {
894 -          System.out.println("'jvmmempc' value must be in range 0 to 100 (read as '"+Integer.toString(jvmmempc)+"')");
895 +            addAll(config.getMultiValue(appPrefix + "jvmarg"), _jvmargs);
896          }
897  
898          // get the set of optimum JVM arguments
899          _optimumJvmArgs = config.getMultiValue("optimum_jvmarg");
900  
901          // transfer our application arguments
902 -        String[] appargs = config.getMultiValue(appPrefix + "apparg");
903 -        addAll(appargs, _appargs);
904 +        addAll(config.getMultiValue(appPrefix + "apparg"), _appargs);
905  
906          // add the launch specific application arguments
907          _appargs.addAll(_envc.appArgs);
908 -        
909 +
910          // look for custom arguments
911          fillAssignmentListFromPairs("extra.txt", _txtJvmArgs);
912  
913 -        // determine whether we want to allow offline operation (defaults to false)
914 -        _allowOffline = config.getBoolean("allow_offline");
915 -
916 -        // look for a debug.txt file which causes us to run in java.exe on Windows so that we can
917 -        // obtain a thread dump of the running JVM
918 -        _windebug = getLocalPath("debug.txt").exists();
919 -
920 -        // whether to cache code resources and launch from cache
921 -        _useCodeCache = config.getBoolean("use_code_cache");
922 -        _codeCacheRetentionDays = config.getInt("code_cache_retention_days", 7);
923 -
924 -        // maximum simultaneous downloads
925 -        _maxConcDownloads = Math.max(1, config.getInt("max_concurrent_downloads",
926 -                                                      SysProps.threadPoolSize()));
927 -
928          // extract some info used to configure our child process on macOS
929          _dockName = config.getString("ui.name");
930          _dockIconPath = config.getString("ui.mac_dock_icon", "../desktop.icns");
931 -
932 -        return config;
933      }
934  
935      /**
936 @@ -847,8 +812,7 @@ public class Application
937       * Returns a URL from which the specified path can be fetched. Our application base URL is
938       * properly versioned and combined with the supplied path.
939       */
940 -    public URL getRemoteURL (String path)
941 -        throws MalformedURLException
942 +    public URL getRemoteURL (String path) throws MalformedURLException
943      {
944          return new URL(_vappbase, encodePath(path));
945      }
946 @@ -858,7 +822,7 @@ public class Application
947       */
948      public File getLocalPath (String path)
949      {
950 -        return getLocalPath(getAppDir(), path);
951 +        return new File(getAppDir(), path);
952      }
953  
954      /**
955 @@ -881,8 +845,7 @@ public class Application
956              // if we have an unpacked VM, check the 'release' file for its version
957              Resource vmjar = getJavaVMResource();
958              if (vmjar != null && vmjar.isMarkedValid()) {
959 -                File vmdir = new File(getAppDir(), LaunchUtil.LOCAL_JAVA_DIR);
960 -                File relfile = new File(vmdir, "release");
961 +                File relfile = new File(_javaLocalDir, "release");
962                  if (!relfile.exists()) {
963                      log.warning("Unpacked JVM missing 'release' file. Assuming valid version.");
964                      return true;
965 @@ -939,7 +902,7 @@ public class Application
966      }
967  
968      /**
969 -     * Attempts to redownload the <code>getdown.txt</code> file based on information parsed from a
970 +     * Attempts to redownload the {@code getdown.txt} file based on information parsed from a
971       * previous call to {@link #init}.
972       */
973      public void attemptRecovery (StatusDisplay status)
974 @@ -950,7 +913,7 @@ public class Application
975      }
976  
977      /**
978 -     * Downloads and replaces the <code>getdown.txt</code> and <code>digest.txt</code> files with
979 +     * Downloads and replaces the {@code getdown.txt} and {@code digest.txt} files with
980       * those for the target version of our application.
981       */
982      public void updateMetadata ()
983 @@ -961,7 +924,7 @@ public class Application
984              _vappbase = createVAppBase(_targetVersion);
985          } catch (MalformedURLException mue) {
986              String err = MessageUtil.tcompose("m.invalid_appbase", _appbase);
987 -            throw (IOException) new IOException(err).initCause(mue);
988 +            throw new IOException(err, mue);
989          }
990  
991          try {
992 @@ -1000,7 +963,7 @@ public class Application
993          ArrayList<String> args = new ArrayList<>();
994  
995          // reconstruct the path to the JVM
996 -        args.add(LaunchUtil.getJVMPath(getAppDir(), _windebug || optimum));
997 +        args.add(LaunchUtil.getJVMBinaryPath(_javaLocalDir, SysProps.debug() || optimum));
998  
999          // check whether we're using -jar mode or -classpath mode
1000          boolean dashJarMode = MANIFEST_CLASS.equals(_class);
1001 @@ -1018,14 +981,8 @@ public class Application
1002              args.add("-Xdock:name=" + _dockName);
1003          }
1004  
1005 -        // pass along our proxy settings
1006 -        String proxyHost;
1007 -        if ((proxyHost = System.getProperty("http.proxyHost")) != null) {
1008 -            args.add("-Dhttp.proxyHost=" + proxyHost);
1009 -            args.add("-Dhttp.proxyPort=" + System.getProperty("http.proxyPort"));
1010 -            args.add("-Dhttps.proxyHost=" + proxyHost);
1011 -            args.add("-Dhttps.proxyPort=" + System.getProperty("http.proxyPort"));
1012 -        }
1013 +        // forward our proxy settings
1014 +        conn.addProxyArgs(args);
1015  
1016          // add the marker indicating the app is running in getdown
1017          args.add("-D" + Properties.GETDOWN + "=true");
1018 @@ -1071,32 +1028,11 @@ public class Application
1019              args.add(_class);
1020          }
1021  
1022 -        // almost finally check the startup file arguments
1023 -        for (File f : _startupFiles) {
1024 -          _appargs.add(f.getAbsolutePath());
1025 -          break; // Only add one file to open
1026 -        }
1027 -        
1028 -        // check if one arg with recognised extension
1029 -        if ( _appargs.size() == 1 && _appargs.get(0) != null ) {
1030 -          String filename = _appargs.get(0);
1031 -          String ext = null;
1032 -          int j = filename.lastIndexOf('.');
1033 -          if (j > -1) {
1034 -            ext = filename.substring(j+1);
1035 -          }
1036 -          if (LOCATOR_FILE_EXTENSION.equals(ext.toLowerCase())) {
1037 -            // this file extension should have been dealt with in Getdown class
1038 -          } else {
1039 -            _appargs.add(0, "-open");
1040 -          }
1041 -        }
1042 -
1043          // finally add the application arguments
1044          for (String string : _appargs) {
1045              args.add(processArg(string));
1046          }
1047 -        
1048 +
1049          String[] envp = createEnvironment();
1050          String[] sargs = args.toArray(new String[args.size()]);
1051          log.info("Running " + StringUtil.join(sargs, "\n  "));
1052 @@ -1156,7 +1092,7 @@ public class Application
1053          for (String jvmarg : _jvmargs) {
1054              if (jvmarg.startsWith("-D")) {
1055                  jvmarg = processArg(jvmarg.substring(2));
1056 -                int eqidx = jvmarg.indexOf("=");
1057 +                int eqidx = jvmarg.indexOf('=');
1058                  if (eqidx == -1) {
1059                      log.warning("Bogus system property: '" + jvmarg + "'?");
1060                  } else {
1061 @@ -1194,32 +1130,36 @@ public class Application
1062          }
1063      }
1064  
1065 -    /** Replaces the application directory and version in any argument. */
1066 +    /** Replaces the application directory, version and env vars in any argument. */
1067      protected String processArg (String arg)
1068      {
1069          arg = arg.replace("%APPDIR%", getAppDir().getAbsolutePath());
1070          arg = arg.replace("%VERSION%", String.valueOf(_version));
1071 +        arg = resolveEnvVars(arg);
1072 +        return arg;
1073 +    }
1074  
1075 -        // if this argument contains %ENV.FOO% replace those with the associated values looked up
1076 -        // from the environment
1077 -        if (arg.contains(ENV_VAR_PREFIX)) {
1078 +    /** Resolves env var substitutions in {@code text}. */
1079 +    protected String resolveEnvVars (String text) {
1080 +        // if the text contains %ENV.FOO% replace it with FOO looked up in the environment
1081 +        if (text.contains(ENV_VAR_PREFIX)) {
1082              StringBuffer sb = new StringBuffer();
1083 -            Matcher matcher = ENV_VAR_PATTERN.matcher(arg);
1084 +            Matcher matcher = ENV_VAR_PATTERN.matcher(text);
1085              while (matcher.find()) {
1086                  String varName = matcher.group(1), varValue = System.getenv(varName);
1087                  String repValue = varValue == null ? "MISSING:"+varName : varValue;
1088                  matcher.appendReplacement(sb, Matcher.quoteReplacement(repValue));
1089              }
1090              matcher.appendTail(sb);
1091 -            arg = sb.toString();
1092 +            return sb.toString();
1093 +        } else {
1094 +            return text;
1095          }
1096 -
1097 -        return arg;
1098      }
1099  
1100      /**
1101 -     * Loads the <code>digest.txt</code> file and verifies the contents of both that file and the
1102 -     * <code>getdown.text</code> file. Then it loads the <code>version.txt</code> and decides
1103 +     * Loads the {@code digest.txt} file and verifies the contents of both that file and the
1104 +     * {@code getdown.text} file. Then it loads the {@code version.txt} and decides
1105       * whether or not the application needs to be updated or whether we can proceed to verification
1106       * and execution.
1107       *
1108 @@ -1306,11 +1246,11 @@ public class Application
1109              }
1110  
1111              if (_latest != null) {
1112 -                try (InputStream in = ConnectionUtil.open(proxy, _latest, 0, 0).getInputStream();
1113 -                     InputStreamReader reader = new InputStreamReader(in, UTF_8);
1114 -                     BufferedReader bin = new BufferedReader(reader)) {
1115 -                    for (String[] pair : Config.parsePairs(bin, Config.createOpts(false))) {
1116 -                        if (pair[0].equals("version")) {
1117 +                try {
1118 +                    List<String[]> vdata = Config.parsePairs(
1119 +                        new StringReader(conn.fetch(_latest)), Config.createOpts(false));
1120 +                    for (String[] pair : vdata) {
1121 +                        if ("version".equals(pair[0])) {
1122                              _targetVersion = Math.max(Long.parseLong(pair[1]), _targetVersion);
1123                              if (fileVersion != -1 && _targetVersion > fileVersion) {
1124                                  // replace the file with the newest version
1125 @@ -1404,7 +1344,10 @@ public class Application
1126          while (completed[0] < rsrcs.size()) {
1127              // we should be getting progress completion updates WAY more often than one every
1128              // minute, so if things freeze up for 60 seconds, abandon ship
1129 -            Runnable action = actions.poll(60, TimeUnit.SECONDS);
1130 +            Runnable action = actions.poll(_verifyTimeout, TimeUnit.SECONDS);
1131 +            if (action == null) {
1132 +                throw new IllegalStateException("m.verify_timeout");
1133 +            }
1134              action.run();
1135          }
1136  
1137 @@ -1415,14 +1358,14 @@ public class Application
1138          unpacked.addAll(unpackedAsync);
1139  
1140          long complete = System.currentTimeMillis();
1141 -        log.info("Verified resources", "count", rsrcs.size(), "size", (totalSize/1024) + "k",
1142 -                 "duration", (complete-start) + "ms");
1143 +        log.info("Verified resources", "count", rsrcs.size(), "alreadyValid", alreadyValid[0],
1144 +                 "size", (totalSize/1024) + "k", "duration", (complete-start) + "ms");
1145      }
1146  
1147      private void verifyResource (Resource rsrc, ProgressObserver obs, int[] alreadyValid,
1148                                   Set<Resource> unpacked,
1149                                   Set<Resource> toInstall, Set<Resource> toDownload) {
1150 -        if (rsrc.isMarkedValid()) {
1151 +        if (_revalidatePolicy != RevalidatePolicy.ALWAYS && rsrc.isMarkedValid()) {
1152              if (alreadyValid != null) {
1153                  alreadyValid[0]++;
1154              }
1155 @@ -1513,7 +1456,7 @@ public class Application
1156      protected URL createVAppBase (long version)
1157          throws MalformedURLException
1158      {
1159 -        String url = version < 0 ? _appbase : _appbase.replace("%VERSION%", "" + version);
1160 +        String url = version < 0 ? _appbase : _appbase.replace("%VERSION%", String.valueOf(version));
1161          return HostWhitelist.verify(new URL(url));
1162      }
1163  
1164 @@ -1530,8 +1473,7 @@ public class Application
1165      /**
1166       * Downloads a new copy of CONFIG_FILE.
1167       */
1168 -    protected void downloadConfigFile ()
1169 -        throws IOException
1170 +    protected void downloadConfigFile () throws IOException
1171      {
1172          downloadControlFile(CONFIG_FILE, 0);
1173      }
1174 @@ -1673,8 +1615,7 @@ public class Application
1175       * Download a path to a temporary file, returning a {@link File} instance with the path
1176       * contents.
1177       */
1178 -    protected File downloadFile (String path)
1179 -        throws IOException
1180 +    protected File downloadFile (String path) throws IOException
1181      {
1182          File target = getLocalPath(path + "_new");
1183  
1184 @@ -1684,30 +1625,11 @@ public class Application
1185          } catch (Exception e) {
1186              log.warning("Requested to download invalid control file",
1187                  "appbase", _vappbase, "path", path, "error", e);
1188 -            throw (IOException) new IOException("Invalid path '" + path + "'.").initCause(e);
1189 +            throw new IOException("Invalid path '" + path + "'.", e);
1190          }
1191  
1192          log.info("Attempting to refetch '" + path + "' from '" + targetURL + "'.");
1193 -
1194 -        // stream the URL into our temporary file
1195 -        URLConnection uconn = ConnectionUtil.open(proxy, targetURL, 0, 0);
1196 -        // we have to tell Java not to use caches here, otherwise it will cache any request for
1197 -        // same URL for the lifetime of this JVM (based on the URL string, not the URL object);
1198 -        // if the getdown.txt file, for example, changes in the meanwhile, we would never hear
1199 -        // about it; turning off caches is not a performance concern, because when Getdown asks
1200 -        // to download a file, it expects it to come over the wire, not from a cache
1201 -        uconn.setUseCaches(false);
1202 -        uconn.setRequestProperty("Accept-Encoding", "gzip");
1203 -        try (InputStream fin = uconn.getInputStream()) {
1204 -            String encoding = uconn.getContentEncoding();
1205 -            boolean gzip = "gzip".equalsIgnoreCase(encoding);
1206 -            try (InputStream fin2 = (gzip ? new GZIPInputStream(fin) : fin)) {
1207 -                try (FileOutputStream fout = new FileOutputStream(target)) {
1208 -                    StreamUtil.copy(fin2, fout);
1209 -                }
1210 -            }
1211 -        }
1212 -
1213 +        conn.download(targetURL, target); // stream the URL into our temporary file
1214          return target;
1215      }
1216  
1217 @@ -1721,9 +1643,7 @@ public class Application
1218      /** Helper function to add all values in {@code values} (if non-null) to {@code target}. */
1219      protected static void addAll (String[] values, List<String> target) {
1220          if (values != null) {
1221 -            for (String value : values) {
1222 -                target.add(value);
1223 -            }
1224 +            Collections.addAll(target, values);
1225          }
1226      }
1227  
1228 @@ -1805,96 +1725,7 @@ public class Application
1229          }
1230      }
1231  
1232 -    protected File getLocalPath (File appdir, String path)
1233 -    {
1234 -        return new File(appdir, path);
1235 -    }
1236 -
1237 -    public static void setStartupFilesFromParameterString(String p) {
1238 -      // multiple files *might* be passed in as space separated quoted filenames
1239 -      String q = "\"";
1240 -      if (!StringUtil.isBlank(p)) {
1241 -        String[] filenames;
1242 -        // split quoted params or treat as single string array
1243 -        if (p.startsWith(q) && p.endsWith(q)) {
1244 -          // this fails if, e.g.
1245 -          // p=q("stupidfilename\" " "otherfilename")
1246 -          // let's hope no-one ever ends a filename with '" '
1247 -          filenames = p.substring(q.length(),p.length()-q.length()).split(q+" "+q);
1248 -        } else {
1249 -          // single unquoted filename
1250 -          filenames = new String[]{p};
1251 -        }
1252 -
1253 -        // check for locator file.  Only allow one locator file to be double clicked (if multiple files opened, ignore locator files)
1254 -        String locatorFilename = filenames.length >= 1 ? filenames[0] : null;
1255 -        if (
1256 -                !StringUtil.isBlank(locatorFilename)
1257 -                && locatorFilename.toLowerCase().endsWith("."+Application.LOCATOR_FILE_EXTENSION)
1258 -                ) {
1259 -          setLocatorFile(locatorFilename);
1260 -          // remove the locator filename from the filenames array
1261 -          String[] otherFilenames = new String[filenames.length - 1];
1262 -          System.arraycopy(filenames, 1, otherFilenames, 0, otherFilenames.length);
1263 -          filenames = otherFilenames;
1264 -        }
1265 -
1266 -        for (int i = 0; i < filenames.length; i++) {
1267 -          String filename = filenames[i];
1268 -          // skip any other locator files in a multiple file list
1269 -          if (! filename.toLowerCase().endsWith("."+Application.LOCATOR_FILE_EXTENSION)) {
1270 -            addStartupFile(filename);
1271 -          }
1272 -        }
1273 -      }
1274 -    }
1275 -    
1276 -    public static void setLocatorFile(String filename) {
1277 -      _locatorFile = new File(filename);
1278 -    }
1279 -    
1280 -    public static void addStartupFile(String filename) {
1281 -      _startupFiles.add(new File(filename));
1282 -    }
1283 -    
1284 -    private Config createLocatorConfig(Config.ParseOpts opts) {
1285 -      if (_locatorFile == null) {
1286 -        return null;
1287 -      }
1288 -      
1289 -      Config locatorConfig = null;
1290 -      
1291 -      try {
1292 -        Config tmpConfig = null;
1293 -        if (_locatorFile.exists()) {
1294 -          tmpConfig = Config.parseConfig(_locatorFile,  opts);
1295 -        } else {
1296 -          log.warning("Given locator file does not exist", "file", _locatorFile);
1297 -        }
1298 -        
1299 -        // appbase is sanitised in HostWhitelist
1300 -        Map<String, Object> tmpData = new HashMap<>();
1301 -        for (Map.Entry<String, Object> entry : tmpConfig.getData().entrySet()) {
1302 -          String key = entry.getKey();
1303 -          Object value = entry.getValue();
1304 -          String mkey = key.indexOf('.') > -1 ? key.substring(key.indexOf('.') + 1) : key;
1305 -          if (Config.allowedReplaceKeys.contains(mkey) || Config.allowedMergeKeys.contains(mkey)) {
1306 -            tmpData.put(key, value);
1307 -          }
1308 -        }
1309 -        locatorConfig = new Config(tmpData);
1310 -        
1311 -      } catch (Exception e) {
1312 -        log.warning("Failure reading locator file",  "file", _locatorFile, e);
1313 -      }
1314 -      
1315 -      log.info("Returning locatorConfig", locatorConfig);
1316 -      
1317 -      return locatorConfig;
1318 -    }
1319 -    
1320      protected final EnvConfig _envc;
1321 -    protected File _config;
1322      protected Digest _digest;
1323  
1324      protected long _version = -1;
1325 @@ -1906,7 +1737,6 @@ public class Application
1326      protected String _dockName;
1327      protected String _dockIconPath;
1328      protected boolean _strictComments;
1329 -    protected boolean _windebug;
1330      protected boolean _allowOffline;
1331      protected int _maxConcDownloads;
1332  
1333 @@ -1924,10 +1754,14 @@ public class Application
1334      protected long _javaMinVersion, _javaMaxVersion;
1335      protected boolean _javaExactVersionRequired;
1336      protected String _javaLocation;
1337 +    protected File _javaLocalDir;
1338  
1339      protected List<Resource> _codes = new ArrayList<>();
1340      protected List<Resource> _resources = new ArrayList<>();
1341  
1342 +    protected int _verifyTimeout = 60;
1343 +
1344 +    protected RevalidatePolicy _revalidatePolicy = RevalidatePolicy.AFTER_UPDATE;
1345      protected boolean _useCodeCache;
1346      protected int _codeCacheRetentionDays;
1347  
1348 @@ -1941,9 +1775,6 @@ public class Application
1349  
1350      protected List<String> _txtJvmArgs = new ArrayList<>();
1351  
1352 -    /** If a warning has been issued about not being able to set modtimes. */
1353 -    protected boolean _warnedAboutSetLastModified;
1354 -
1355      /** Locks gettingdown.lock in the app dir. Held the entire time updating is going on.*/
1356      protected FileLock _lock;
1357  
1358 @@ -1956,8 +1787,6 @@ public class Application
1359  
1360      protected static final String ENV_VAR_PREFIX = "%ENV.";
1361      protected static final Pattern ENV_VAR_PATTERN = Pattern.compile("%ENV\\.(.*?)%");
1362
1363 -    protected static File _locatorFile;
1364 -    protected static List<File> _startupFiles = new ArrayList<>();
1365 -    public static final String LOCATOR_FILE_EXTENSION = "jvl";
1366 +
1367 +    protected static enum RevalidatePolicy { ALWAYS, AFTER_UPDATE }
1368  }
1369 diff --git a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/data/Digest.java b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/data/Digest.java
1370 index bc8d14052..e310a52a2 100644
1371 --- a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/data/Digest.java
1372 +++ b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/data/Digest.java
1373 @@ -21,7 +21,7 @@ import static com.threerings.getdown.Log.log;
1374  import static java.nio.charset.StandardCharsets.UTF_8;
1375  
1376  /**
1377 - * Manages the <code>digest.txt</code> file and the computing and processing of digests for an
1378 + * Manages the {@code digest.txt} file and the computing and processing of digests for an
1379   * application.
1380   */
1381  public class Digest
1382 @@ -72,8 +72,7 @@ public class Digest
1383                          digests.put(rsrc, rsrc.computeDigest(fversion, md, null));
1384                          completed.add(rsrc);
1385                      } catch (Throwable t) {
1386 -                        completed.add(new IOException("Error computing digest for: " + rsrc).
1387 -                                      initCause(t));
1388 +                        completed.add(new IOException("Error computing digest for: " + rsrc, t));
1389                      }
1390                  }
1391              });
1392 @@ -88,7 +87,7 @@ public class Digest
1393                  if (done instanceof IOException) {
1394                      throw (IOException)done;
1395                  } else if (done instanceof Resource) {
1396 -                    pending.remove((Resource)done);
1397 +                    pending.remove(done);
1398                  } else {
1399                      throw new AssertionError("What is this? " + done);
1400                  }
1401 diff --git a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/data/EnvConfig.java b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/data/EnvConfig.java
1402 index 57b8d8493..a14b02c63 100644
1403 --- a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/data/EnvConfig.java
1404 +++ b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/data/EnvConfig.java
1405 @@ -7,22 +7,19 @@ package com.threerings.getdown.data;
1406  
1407  import java.io.File;
1408  import java.io.FileInputStream;
1409 -import java.net.MalformedURLException;
1410 -import java.net.URL;
1411  import java.security.cert.Certificate;
1412  import java.security.cert.CertificateFactory;
1413  import java.security.cert.X509Certificate;
1414  import java.util.*;
1415  
1416  import com.threerings.getdown.util.StringUtil;
1417 -import com.threerings.getdown.data.Application;
1418  
1419  /** Configuration that comes from our "environment" (command line args, sys props, etc.). */
1420  public final class EnvConfig {
1421  
1422      /** Used to report problems or feedback by {@link #create}. */
1423      public static final class Note {
1424 -        public static enum Level { INFO, WARN, ERROR };
1425 +        public enum Level { INFO, WARN, ERROR }
1426          public static Note info (String msg) { return new Note(Level.INFO, msg); }
1427          public static Note warn (String msg) { return new Note(Level.WARN, msg); }
1428          public static Note error (String msg) { return new Note(Level.ERROR, msg); }
1429 @@ -141,23 +138,11 @@ public final class EnvConfig {
1430                                      appIdProv + "'"));
1431              }
1432          }
1433 -        
1434 -        int skipArgs = 2;
1435 -        // Look for locator file, pass to Application and remove from appArgs
1436 -        String argvLocatorFilename = argv.length > 2 ? argv[2] : null;
1437 -        if (
1438 -                !StringUtil.isBlank(argvLocatorFilename)
1439 -                && argvLocatorFilename.toLowerCase().endsWith("."+Application.LOCATOR_FILE_EXTENSION)
1440 -                ) {
1441 -          notes.add(Note.info("locatorFilename in args: '"+argv[2]+"'"));
1442 -          Application.setLocatorFile(argvLocatorFilename);
1443 -          
1444 -          skipArgs++;
1445 -        }
1446  
1447 -        // ensure that we were able to find an app dir
1448 +        // if no appdir was provided, default to the current working directory
1449          if (appDir == null) {
1450 -            return null; // caller will report problem to user
1451 +            appDir = System.getProperty("user.dir");
1452 +            appDirProv = "default (cwd)";
1453          }
1454  
1455          notes.add(Note.info("Using appdir from " + appDirProv + ": " + appDir));
1456 @@ -187,9 +172,9 @@ public final class EnvConfig {
1457              return null;
1458          }
1459  
1460 -        // pass along anything after the first two (or three) args as extra app args
1461 -        List<String> appArgs = argv.length > skipArgs ?
1462 -            Arrays.asList(argv).subList(skipArgs, argv.length) :
1463 +        // pass along anything after the first two args as extra app args
1464 +        List<String> appArgs = argv.length > 2 ?
1465 +            Arrays.asList(argv).subList(2, argv.length) :
1466              Collections.<String>emptyList();
1467  
1468          // load X.509 certificate if it exists
1469 diff --git a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/data/PathBuilder.java b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/data/PathBuilder.java
1470 index b0a1dc920..57e9275be 100644
1471 --- a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/data/PathBuilder.java
1472 +++ b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/data/PathBuilder.java
1473 @@ -10,7 +10,7 @@ import java.io.IOException;
1474  import java.util.LinkedHashSet;
1475  import java.util.List;
1476  import java.util.concurrent.TimeUnit;
1477 -import java.util.jar.JarFile;
1478 +import java.util.zip.ZipFile;
1479  
1480  import com.threerings.getdown.cache.GarbageCollector;
1481  import com.threerings.getdown.cache.ResourceCache;
1482 @@ -112,7 +112,7 @@ public class PathBuilder
1483  
1484              if (!unpackedIndicator.exists()) {
1485                  try {
1486 -                    FileUtil.unpackJar(new JarFile(cachedFile), cachedParent, false);
1487 +                    FileUtil.unpackJar(new ZipFile(cachedFile), cachedParent, false);
1488                      unpackedIndicator.createNewFile();
1489                  } catch (IOException ioe) {
1490                      log.warning("Failed to unpack native jar",
1491 diff --git a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/data/Resource.java b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/data/Resource.java
1492 index adc2d4f21..d1ccba3ba 100644
1493 --- a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/data/Resource.java
1494 +++ b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/data/Resource.java
1495 @@ -7,25 +7,14 @@ package com.threerings.getdown.data;
1496  
1497  import java.io.*;
1498  import java.net.URL;
1499 -import java.nio.file.Files;
1500 -import java.nio.file.Paths;
1501  import java.security.MessageDigest;
1502 -import java.util.Collections;
1503 -import java.util.Comparator;
1504 -import java.util.EnumSet;
1505 -import java.util.List;
1506 -import java.util.Locale;
1507 -import java.util.jar.JarEntry;
1508 -import java.util.jar.JarFile;
1509 -
1510 -import org.apache.commons.compress.archivers.ArchiveInputStream;
1511 -import org.apache.commons.compress.archivers.tar.TarArchiveInputStream;
1512 -import org.apache.commons.compress.compressors.gzip.GzipCompressorInputStream;
1513 +import java.util.*;
1514 +import java.util.zip.ZipEntry;
1515 +import java.util.zip.ZipFile;
1516  
1517  import com.threerings.getdown.util.FileUtil;
1518  import com.threerings.getdown.util.ProgressObserver;
1519  import com.threerings.getdown.util.StringUtil;
1520 -
1521  import static com.threerings.getdown.Log.log;
1522  
1523  /**
1524 @@ -34,7 +23,7 @@ import static com.threerings.getdown.Log.log;
1525  public class Resource implements Comparable<Resource>
1526  {
1527      /** Defines special attributes for resources. */
1528 -    public static enum Attr {
1529 +    public enum Attr {
1530          /** Indicates that the resource should be unpacked. */
1531          UNPACK,
1532          /** If present, when unpacking a resource, any directories created by the newly
1533 @@ -46,7 +35,7 @@ public class Resource implements Comparable<Resource>
1534          PRELOAD,
1535          /** Indicates that the resource is a jar containing native libs. */
1536          NATIVE
1537 -    };
1538 +    }
1539  
1540      public static final EnumSet<Attr> NORMAL  = EnumSet.noneOf(Attr.class);
1541      public static final EnumSet<Attr> UNPACK  = EnumSet.of(Attr.UNPACK);
1542 @@ -66,29 +55,30 @@ public class Resource implements Comparable<Resource>
1543          byte[] buffer = new byte[DIGEST_BUFFER_SIZE];
1544          int read;
1545  
1546 -        boolean isJar = isJar(target.getPath());
1547 -        boolean isPacked200Jar = isPacked200Jar(target.getPath());
1548 +        boolean isZip = isJar(target) || isZip(target); // jar is a zip too
1549 +        boolean isPacked200Jar = isPacked200Jar(target);
1550  
1551          // if this is a jar, we need to compute the digest in a "timestamp and file order" agnostic
1552          // manner to properly correlate jardiff patched jars with their unpatched originals
1553 -        if (isJar || isPacked200Jar){
1554 +        if (isPacked200Jar || isZip){
1555              File tmpJarFile = null;
1556 -            JarFile jar = null;
1557 +            ZipFile zip = null;
1558              try {
1559 -                // if this is a compressed jar file, uncompress it to compute the jar file digest
1560 +                // if this is a compressed zip file, uncompress it to compute the zip file digest
1561                  if (isPacked200Jar){
1562                      tmpJarFile = new File(target.getPath() + ".tmp");
1563 +                    tmpJarFile.deleteOnExit();
1564                      FileUtil.unpackPacked200Jar(target, tmpJarFile);
1565 -                    jar = new JarFile(tmpJarFile);
1566 +                    zip = new ZipFile(tmpJarFile);
1567                  } else{
1568 -                    jar = new JarFile(target);
1569 +                    zip = new ZipFile(target);
1570                  }
1571  
1572 -                List<JarEntry> entries = Collections.list(jar.entries());
1573 +                List<? extends ZipEntry> entries = Collections.list(zip.entries());
1574                  Collections.sort(entries, ENTRY_COMP);
1575  
1576                  int eidx = 0;
1577 -                for (JarEntry entry : entries) {
1578 +                for (ZipEntry entry : entries) {
1579                      // old versions of the digest code skipped metadata
1580                      if (version < 2) {
1581                          if (entry.getName().startsWith("META-INF")) {
1582 @@ -97,7 +87,7 @@ public class Resource implements Comparable<Resource>
1583                          }
1584                      }
1585  
1586 -                    try (InputStream in = jar.getInputStream(entry)) {
1587 +                    try (InputStream in = zip.getInputStream(entry)) {
1588                          while ((read = in.read(buffer)) != -1) {
1589                              md.update(buffer, 0, read);
1590                          }
1591 @@ -107,11 +97,11 @@ public class Resource implements Comparable<Resource>
1592                  }
1593  
1594              } finally {
1595 -                if (jar != null) {
1596 +                if (zip != null) {
1597                      try {
1598 -                        jar.close();
1599 +                        zip.close();
1600                      } catch (IOException ioe) {
1601 -                        log.warning("Error closing jar", "path", target, "jar", jar, "error", ioe);
1602 +                        log.warning("Error closing", "path", target, "zip", zip, "error", ioe);
1603                      }
1604                  }
1605                  if (tmpJarFile != null) {
1606 @@ -132,6 +122,34 @@ public class Resource implements Comparable<Resource>
1607          return StringUtil.hexlate(md.digest());
1608      }
1609  
1610 +    /**
1611 +     * Returns whether {@code file} is a {@code zip} file.
1612 +     */
1613 +    public static boolean isZip (File file)
1614 +    {
1615 +        String path = file.getName();
1616 +        return path.endsWith(".zip") || path.endsWith(".zip_new");
1617 +    }
1618 +
1619 +    /**
1620 +     * Returns whether {@code file} is a {@code jar} file.
1621 +     */
1622 +    public static boolean isJar (File file)
1623 +    {
1624 +        String path = file.getName();
1625 +        return path.endsWith(".jar") || path.endsWith(".jar_new");
1626 +    }
1627 +
1628 +    /**
1629 +     * Returns whether {@code file} is a {@code jar.pack} file.
1630 +     */
1631 +    public static boolean isPacked200Jar (File file)
1632 +    {
1633 +        String path = file.getName();
1634 +        return path.endsWith(".jar.pack") || path.endsWith(".jar.pack_new") ||
1635 +            path.endsWith(".jar.pack.gz") || path.endsWith(".jar.pack.gz_new");
1636 +    }
1637 +
1638      /**
1639       * Creates a resource with the supplied remote URL and local path.
1640       */
1641 @@ -141,17 +159,13 @@ public class Resource implements Comparable<Resource>
1642          _remote = remote;
1643          _local = local;
1644          _localNew = new File(local.toString() + "_new");
1645 -        String lpath = _local.getPath();
1646 -        _marker = new File(lpath + "v");
1647 +        _marker = new File(_local.getPath() + "v");
1648  
1649          _attrs = attrs;
1650 -        _isTgz = isTgz(lpath);
1651 -        _isJar = isJar(lpath);
1652 -        _isPacked200Jar = isPacked200Jar(lpath);
1653 +        _isZip = isJar(local) || isZip(local);
1654 +        _isPacked200Jar = isPacked200Jar(local);
1655          boolean unpack = attrs.contains(Attr.UNPACK);
1656 -        if (unpack && _isJar) {
1657 -            _unpacked = _local.getParentFile();
1658 -        } else if(unpack && _isTgz) {
1659 +        if (unpack && _isZip) {
1660              _unpacked = _local.getParentFile();
1661          } else if(unpack && _isPacked200Jar) {
1662              String dotJar = ".jar", lname = _local.getName();
1663 @@ -307,20 +321,13 @@ public class Resource implements Comparable<Resource>
1664      public void unpack () throws IOException
1665      {
1666          // sanity check
1667 -        if (!_isJar && !_isPacked200Jar && !_isTgz) {
1668 -            throw new IOException("Requested to unpack non-jar/tgz file '" + _local + "'.");
1669 +        if (!_isZip && !_isPacked200Jar) {
1670 +            throw new IOException("Requested to unpack non-jar file '" + _local + "'.");
1671          }
1672 -        if (_isJar) {
1673 -            try (JarFile jar = new JarFile(_local)) {
1674 +        if (_isZip) {
1675 +            try (ZipFile jar = new ZipFile(_local)) {
1676                  FileUtil.unpackJar(jar, _unpacked, _attrs.contains(Attr.CLEAN));
1677              }
1678 -        } else if (_isTgz) {
1679 -            try (InputStream fi = Files.newInputStream(_local.toPath());
1680 -                         InputStream bi = new BufferedInputStream(fi);
1681 -                         InputStream gzi = new GzipCompressorInputStream(bi);
1682 -                         TarArchiveInputStream tgz = new TarArchiveInputStream(gzi)) {
1683 -                    FileUtil.unpackTgz(tgz, _unpacked, _attrs.contains(Attr.CLEAN));
1684 -            }
1685          } else {
1686              FileUtil.unpackPacked200Jar(_local, _unpacked);
1687          }
1688 @@ -382,31 +389,15 @@ public class Resource implements Comparable<Resource>
1689          }
1690      }
1691  
1692 -    protected static boolean isJar (String path)
1693 -    {
1694 -        return path.endsWith(".jar") || path.endsWith(".jar_new");
1695 -    }
1696 -    
1697 -    protected static boolean isTgz (String path)
1698 -    {
1699 -        return path.endsWith(".tgz") || path.endsWith(".tgz_new");
1700 -    }
1701 -
1702 -    protected static boolean isPacked200Jar (String path)
1703 -    {
1704 -        return path.endsWith(".jar.pack") || path.endsWith(".jar.pack_new") ||
1705 -            path.endsWith(".jar.pack.gz")|| path.endsWith(".jar.pack.gz_new");
1706 -    }
1707 -
1708      protected String _path;
1709      protected URL _remote;
1710      protected File _local, _localNew, _marker, _unpacked;
1711      protected EnumSet<Attr> _attrs;
1712 -    protected boolean _isJar, _isPacked200Jar, _isTgz;
1713 +    protected boolean _isZip, _isPacked200Jar;
1714  
1715      /** Used to sort the entries in a jar file. */
1716 -    protected static final Comparator<JarEntry> ENTRY_COMP = new Comparator<JarEntry>() {
1717 -        @Override public int compare (JarEntry e1, JarEntry e2) {
1718 +    protected static final Comparator<ZipEntry> ENTRY_COMP = new Comparator<ZipEntry>() {
1719 +        @Override public int compare (ZipEntry e1, ZipEntry e2) {
1720              return e1.getName().compareTo(e2.getName());
1721          }
1722      };
1723 diff --git a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/data/SysProps.java b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/data/SysProps.java
1724 index 0d96ecb71..b36d40021 100644
1725 --- a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/data/SysProps.java
1726 +++ b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/data/SysProps.java
1727 @@ -16,7 +16,7 @@ import com.threerings.getdown.util.VersionUtil;
1728   * accessor so that it's easy to see all of the secret system property arguments that Getdown makes
1729   * use of.
1730   */
1731 -public class SysProps
1732 +public final class SysProps
1733  {
1734      /** Configures the appdir (in lieu of passing it in argv). Usage: {@code -Dappdir=foo}. */
1735      public static String appDir () {
1736 @@ -40,6 +40,14 @@ public class SysProps
1737          return System.getProperty("no_log_redir") != null;
1738      }
1739  
1740 +    /** Used to debug Getdown's launching of an app. When set, it disables redirection of stdout
1741 +      * and stderr into a log file, and on Windows it uses {@code java.exe} to launch the app so
1742 +      * that its console output can be observed.
1743 +      * Usage: {@code -Ddebug}. */
1744 +    public static boolean debug () {
1745 +        return System.getProperty("debug") != null;
1746 +    }
1747 +
1748      /** Overrides the domain on {@code appbase}. Usage: {@code -Dappbase_domain=foo}. */
1749      public static String appbaseDomain () {
1750          return System.getProperty("appbase_domain");
1751 @@ -101,6 +109,16 @@ public class SysProps
1752          return Boolean.getBoolean("direct");
1753      }
1754  
1755 +    /** If true, Getdown will always try to connect without proxy settings even it a proxy is set
1756 +      * in {@code proxy.txt}. If direct access is possible it will not clear {@code proxy.txt}, it
1757 +      * will preserve the settings. This is to support cases where a user uses a workstation in two
1758 +      * different networks, one with proxy the other one without. They should not be asked for
1759 +      * proxy settings again each time they switch back to the proxy network.
1760 +      * Usage: {@code -Dtry_no_proxy}. */
1761 +    public static boolean tryNoProxyFirst () {
1762 +        return Boolean.getBoolean("try_no_proxy");
1763 +    }
1764 +
1765      /** Specifies the connection timeout (in seconds) to use when downloading control files from
1766        * the server. This is chiefly useful when you are running in versionless mode and want Getdown
1767        * to more quickly timeout its startup update check if the server with which it is
1768 diff --git a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/net/Downloader.java b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/net/Downloader.java
1769 index 6033e2f6e..2298d6099 100644
1770 --- a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/net/Downloader.java
1771 +++ b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/net/Downloader.java
1772 @@ -6,7 +6,13 @@
1773  package com.threerings.getdown.net;
1774  
1775  import java.io.File;
1776 +import java.io.FileOutputStream;
1777  import java.io.IOException;
1778 +import java.io.InputStream;
1779 +import java.net.HttpURLConnection;
1780 +import java.net.URLConnection;
1781 +import java.nio.channels.Channels;
1782 +import java.nio.channels.ReadableByteChannel;
1783  
1784  import java.util.Collection;
1785  import java.util.HashMap;
1786 @@ -26,8 +32,13 @@ import static com.threerings.getdown.Log.log;
1787   * implementors must take care to only execute thread-safe code or simply pass a message to the AWT
1788   * thread, for example.
1789   */
1790 -public abstract class Downloader
1791 +public class Downloader
1792  {
1793 +    public Downloader (Connector conn)
1794 +    {
1795 +        _conn = conn;
1796 +    }
1797 +
1798      /**
1799       * Start the downloading process.
1800       * @param resources the resources to download.
1801 @@ -129,10 +140,31 @@ public abstract class Downloader
1802       */
1803      protected void downloadFailed (Resource rsrc, Exception cause) {}
1804  
1805 +    /**
1806 +     * Called when a to-be-downloaded resource returns a 404 not found.
1807 +     */
1808 +    protected void resourceMissing (Resource rsrc) {}
1809 +
1810      /**
1811       * Performs the protocol-specific portion of checking download size.
1812       */
1813 -    protected abstract long checkSize (Resource rsrc) throws IOException;
1814 +    protected long checkSize (Resource rsrc) throws IOException {
1815 +        URLConnection conn = _conn.open(rsrc.getRemote(), 0, 0);
1816 +        try {
1817 +            // if we're accessing our data via HTTP, we only need a HEAD request
1818 +            if (conn instanceof HttpURLConnection) {
1819 +                ((HttpURLConnection)conn).setRequestMethod("HEAD");
1820 +            }
1821 +            // if we get a satisfactory response code, return a size; ignore errors as we'll report
1822 +            // those when we actually attempt to download the resource
1823 +            int code = _conn.checkConnectStatus(conn);
1824 +            return code == HttpURLConnection.HTTP_OK ? conn.getContentLength() : 0;
1825 +
1826 +        } finally {
1827 +            // let it be known that we're done with this connection
1828 +            conn.getInputStream().close();
1829 +        }
1830 +    }
1831  
1832      /**
1833       * Periodically called by the protocol-specific downloaders to update their progress. This
1834 @@ -203,13 +235,68 @@ public abstract class Downloader
1835       * protocol-specific code. This method should periodically check whether {@code _state} is set
1836       * to aborted and abort any in-progress download if so.
1837       */
1838 -    protected abstract void download (Resource rsrc) throws IOException;
1839 +    protected void download (Resource rsrc) throws IOException {
1840 +        URLConnection conn = _conn.open(rsrc.getRemote(), 0, 0);
1841 +        // make sure we got a satisfactory response code
1842 +        int code = _conn.checkConnectStatus(conn);
1843 +        if (code == HttpURLConnection.HTTP_NOT_FOUND) {
1844 +            resourceMissing(rsrc);
1845 +        } else if (code != HttpURLConnection.HTTP_OK) {
1846 +            throw new IOException(
1847 +                "Resource returned HTTP error " + rsrc.getRemote() + " [code=" + code + "]");
1848 +        }
1849 +
1850 +        // TODO: make FileChannel download impl (below) robust and allow apps to opt-into it via a
1851 +        // system property
1852 +        if (true) {
1853 +            // download the resource from the specified URL
1854 +            long actualSize = conn.getContentLength();
1855 +            log.info("Downloading resource", "url", rsrc.getRemote(), "size", actualSize);
1856 +            long currentSize = 0L;
1857 +            byte[] buffer = new byte[4*4096];
1858 +            try (InputStream in = conn.getInputStream();
1859 +                 FileOutputStream out = new FileOutputStream(rsrc.getLocalNew())) {
1860 +
1861 +                // TODO: look to see if we have a download info file
1862 +                // containing info on potentially partially downloaded data;
1863 +                // if so, use a "Range: bytes=HAVE-" header.
1864 +
1865 +                // read in the file data
1866 +                int read;
1867 +                while ((read = in.read(buffer)) != -1) {
1868 +                    // abort the download if the downloader is aborted
1869 +                    if (_state == State.ABORTED) {
1870 +                        break;
1871 +                    }
1872 +                    // write it out to our local copy
1873 +                    out.write(buffer, 0, read);
1874 +                    // note that we've downloaded some data
1875 +                    currentSize += read;
1876 +                    reportProgress(rsrc, currentSize, actualSize);
1877 +                }
1878 +            }
1879 +
1880 +        } else {
1881 +            log.info("Downloading resource", "url", rsrc.getRemote(), "size", "unknown");
1882 +            File localNew = rsrc.getLocalNew();
1883 +            try (ReadableByteChannel rbc = Channels.newChannel(conn.getInputStream());
1884 +                 FileOutputStream fos = new FileOutputStream(localNew)) {
1885 +                // TODO: more work is needed here, transferFrom can fail to transfer the entire
1886 +                // file, in which case it's not clear what we're supposed to do.. call it again?
1887 +                // will it repeatedly fail?
1888 +                fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE);
1889 +                reportProgress(rsrc, localNew.length(), localNew.length());
1890 +            }
1891 +        }
1892 +    }
1893 +
1894 +    protected final Connector _conn;
1895  
1896      /** The reported sizes of our resources. */
1897 -    protected Map<Resource, Long> _sizes = new HashMap<>();
1898 +    protected final Map<Resource, Long> _sizes = new HashMap<>();
1899  
1900      /** The bytes downloaded for each resource. */
1901 -    protected Map<Resource, Long> _downloaded = new HashMap<>();
1902 +    protected final Map<Resource, Long> _downloaded = new HashMap<>();
1903  
1904      /** The time at which the file transfer began. */
1905      protected long _start;
1906 diff --git a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/net/HTTPDownloader.java b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/net/HTTPDownloader.java
1907 deleted file mode 100644
1908 index a7a3287a9..000000000
1909 --- a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/net/HTTPDownloader.java
1910 +++ /dev/null
1911 @@ -1,115 +0,0 @@
1912 -//
1913 -// Getdown - application installer, patcher and launcher
1914 -// Copyright (C) 2004-2018 Getdown authors
1915 -// https://github.com/threerings/getdown/blob/master/LICENSE
1916 -
1917 -package com.threerings.getdown.net;
1918 -
1919 -import java.io.File;
1920 -import java.io.FileOutputStream;
1921 -import java.io.IOException;
1922 -import java.io.InputStream;
1923 -import java.net.HttpURLConnection;
1924 -import java.net.Proxy;
1925 -import java.net.URL;
1926 -import java.net.URLConnection;
1927 -import java.nio.channels.Channels;
1928 -import java.nio.channels.ReadableByteChannel;
1929 -
1930 -import com.threerings.getdown.data.Resource;
1931 -import com.threerings.getdown.util.ConnectionUtil;
1932 -
1933 -import static com.threerings.getdown.Log.log;
1934 -
1935 -/**
1936 - * Implements downloading files over HTTP
1937 - */
1938 -public class HTTPDownloader extends Downloader
1939 -{
1940 -    public HTTPDownloader (Proxy proxy)
1941 -    {
1942 -        _proxy = proxy;
1943 -    }
1944 -
1945 -    @Override protected long checkSize (Resource rsrc) throws IOException
1946 -    {
1947 -        URLConnection conn = ConnectionUtil.open(_proxy, rsrc.getRemote(), 0, 0);
1948 -        try {
1949 -            // if we're accessing our data via HTTP, we only need a HEAD request
1950 -            if (conn instanceof HttpURLConnection) {
1951 -                HttpURLConnection hcon = (HttpURLConnection)conn;
1952 -                hcon.setRequestMethod("HEAD");
1953 -                hcon.connect();
1954 -                // make sure we got a satisfactory response code
1955 -                if (hcon.getResponseCode() != HttpURLConnection.HTTP_OK) {
1956 -                    throw new IOException("Unable to check up-to-date for " +
1957 -                                          rsrc.getRemote() + ": " + hcon.getResponseCode());
1958 -                }
1959 -            }
1960 -            return conn.getContentLength();
1961 -
1962 -        } finally {
1963 -            // let it be known that we're done with this connection
1964 -            conn.getInputStream().close();
1965 -        }
1966 -    }
1967 -
1968 -    @Override protected void download (Resource rsrc) throws IOException
1969 -    {
1970 -        // TODO: make FileChannel download impl (below) robust and allow apps to opt-into it via a
1971 -        // system property
1972 -        if (true) {
1973 -            // download the resource from the specified URL
1974 -            URLConnection conn = ConnectionUtil.open(_proxy, rsrc.getRemote(), 0, 0);
1975 -            conn.connect();
1976 -
1977 -            // make sure we got a satisfactory response code
1978 -            if (conn instanceof HttpURLConnection) {
1979 -                HttpURLConnection hcon = (HttpURLConnection)conn;
1980 -                if (hcon.getResponseCode() != HttpURLConnection.HTTP_OK) {
1981 -                    throw new IOException("Unable to download resource " + rsrc.getRemote() + ": " +
1982 -                                          hcon.getResponseCode());
1983 -                }
1984 -            }
1985 -            long actualSize = conn.getContentLength();
1986 -            log.info("Downloading resource", "url", rsrc.getRemote(), "size", actualSize);
1987 -            long currentSize = 0L;
1988 -            byte[] buffer = new byte[4*4096];
1989 -            try (InputStream in = conn.getInputStream();
1990 -                 FileOutputStream out = new FileOutputStream(rsrc.getLocalNew())) {
1991 -
1992 -                // TODO: look to see if we have a download info file
1993 -                // containing info on potentially partially downloaded data;
1994 -                // if so, use a "Range: bytes=HAVE-" header.
1995 -
1996 -                // read in the file data
1997 -                int read;
1998 -                while ((read = in.read(buffer)) != -1) {
1999 -                    // abort the download if the downloader is aborted
2000 -                    if (_state == State.ABORTED) {
2001 -                        break;
2002 -                    }
2003 -                    // write it out to our local copy
2004 -                    out.write(buffer, 0, read);
2005 -                    // note that we've downloaded some data
2006 -                    currentSize += read;
2007 -                    reportProgress(rsrc, currentSize, actualSize);
2008 -                }
2009 -            }
2010 -
2011 -        } else {
2012 -            log.info("Downloading resource", "url", rsrc.getRemote(), "size", "unknown");
2013 -            File localNew = rsrc.getLocalNew();
2014 -            try (ReadableByteChannel rbc = Channels.newChannel(rsrc.getRemote().openStream());
2015 -                 FileOutputStream fos = new FileOutputStream(localNew)) {
2016 -                // TODO: more work is needed here, transferFrom can fail to transfer the entire
2017 -                // file, in which case it's not clear what we're supposed to do.. call it again?
2018 -                // will it repeatedly fail?
2019 -                fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE);
2020 -                reportProgress(rsrc, localNew.length(), localNew.length());
2021 -            }
2022 -        }
2023 -    }
2024 -
2025 -    protected final Proxy _proxy;
2026 -}
2027 diff --git a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/spi/ProxyAuth.java b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/spi/ProxyAuth.java
2028 index 22446ec0a..8c6d84160 100644
2029 --- a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/spi/ProxyAuth.java
2030 +++ b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/spi/ProxyAuth.java
2031 @@ -11,7 +11,7 @@ package com.threerings.getdown.spi;
2032  public interface ProxyAuth
2033  {
2034      /** Credentials for a proxy server. */
2035 -    public static class Credentials {
2036 +    class Credentials {
2037          public final String username;
2038          public final String password;
2039          public Credentials (String username, String password) {
2040 @@ -23,10 +23,10 @@ public interface ProxyAuth
2041      /**
2042       * Loads the credentials for the app installed in {@code appDir}.
2043       */
2044 -    public Credentials loadCredentials (String appDir);
2045 +    Credentials loadCredentials (String appDir);
2046  
2047      /**
2048       * Encrypts and saves the credentials for the app installed in {@code appDir}.
2049       */
2050 -    public void saveCredentials (String appDir, String username, String password);
2051 +    void saveCredentials (String appDir, String username, String password);
2052  }
2053 diff --git a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/tools/Differ.java b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/tools/Differ.java
2054 index c2e740b6e..4f8e50ebd 100644
2055 --- a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/tools/Differ.java
2056 +++ b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/tools/Differ.java
2057 @@ -11,15 +11,14 @@ import java.io.FileInputStream;
2058  import java.io.FileOutputStream;
2059  import java.io.IOException;
2060  import java.io.InputStream;
2061 -
2062 +import java.io.OutputStream;
2063 +import java.security.MessageDigest;
2064  import java.util.ArrayList;
2065  import java.util.Enumeration;
2066 -import java.util.jar.JarEntry;
2067 -import java.util.jar.JarFile;
2068 -import java.util.jar.JarOutputStream;
2069 +import java.util.List;
2070  import java.util.zip.ZipEntry;
2071 -
2072 -import java.security.MessageDigest;
2073 +import java.util.zip.ZipFile;
2074 +import java.util.zip.ZipOutputStream;
2075  
2076  import com.threerings.getdown.data.Application;
2077  import com.threerings.getdown.data.Digest;
2078 @@ -39,8 +38,8 @@ public class Differ
2079      /**
2080       * Creates a single patch file that contains the differences between
2081       * the two specified application directories. The patch file will be
2082 -     * created in the <code>nvdir</code> directory with name
2083 -     * <code>patchV.dat</code> where V is the old application version.
2084 +     * created in the {@code nvdir} directory with name
2085 +     * {@code patchV.dat} where V is the old application version.
2086       */
2087      public void createDiff (File nvdir, File ovdir, boolean verbose)
2088          throws IOException
2089 @@ -61,13 +60,13 @@ public class Differ
2090  
2091          Application oapp = new Application(new EnvConfig(ovdir));
2092          oapp.init(false);
2093 -        ArrayList<Resource> orsrcs = new ArrayList<>();
2094 +        List<Resource> orsrcs = new ArrayList<>();
2095          orsrcs.addAll(oapp.getCodeResources());
2096          orsrcs.addAll(oapp.getResources());
2097  
2098          Application napp = new Application(new EnvConfig(nvdir));
2099          napp.init(false);
2100 -        ArrayList<Resource> nrsrcs = new ArrayList<>();
2101 +        List<Resource> nrsrcs = new ArrayList<>();
2102          nrsrcs.addAll(napp.getCodeResources());
2103          nrsrcs.addAll(napp.getResources());
2104  
2105 @@ -91,15 +90,15 @@ public class Differ
2106          }
2107      }
2108  
2109 -    protected void createPatch (File patch, ArrayList<Resource> orsrcs,
2110 -                                ArrayList<Resource> nrsrcs, boolean verbose)
2111 +    protected void createPatch (File patch, List<Resource> orsrcs,
2112 +                                List<Resource> nrsrcs, boolean verbose)
2113          throws IOException
2114      {
2115          int version = Digest.VERSION;
2116          MessageDigest md = Digest.getMessageDigest(version);
2117          try (FileOutputStream fos = new FileOutputStream(patch);
2118               BufferedOutputStream buffered = new BufferedOutputStream(fos);
2119 -             JarOutputStream jout = new JarOutputStream(buffered)) {
2120 +             ZipOutputStream jout = new ZipOutputStream(buffered)) {
2121  
2122              // for each file in the new application, it either already exists
2123              // in the old application, or it is new
2124 @@ -172,13 +171,13 @@ public class Differ
2125          throws IOException
2126      {
2127          File temp = File.createTempFile("differ", "jar");
2128 -        try (JarFile jar = new JarFile(target);
2129 +        try (ZipFile jar = new ZipFile(target);
2130               FileOutputStream tempFos = new FileOutputStream(temp);
2131               BufferedOutputStream tempBos = new BufferedOutputStream(tempFos);
2132 -             JarOutputStream jout = new JarOutputStream(tempBos)) {
2133 +             ZipOutputStream jout = new ZipOutputStream(tempBos)) {
2134              byte[] buffer = new byte[4096];
2135 -            for (Enumeration< JarEntry > iter = jar.entries(); iter.hasMoreElements();) {
2136 -                JarEntry entry = iter.nextElement();
2137 +            for (Enumeration<? extends ZipEntry> iter = jar.entries(); iter.hasMoreElements();) {
2138 +                ZipEntry entry = iter.nextElement();
2139                  entry.setCompressedSize(-1);
2140                  jout.putNextEntry(entry);
2141                  try (InputStream in = jar.getInputStream(entry)) {
2142 @@ -193,8 +192,7 @@ public class Differ
2143          return temp;
2144      }
2145  
2146 -    protected void jarDiff (File ofile, File nfile, JarOutputStream jout)
2147 -        throws IOException
2148 +    protected void jarDiff (File ofile, File nfile, ZipOutputStream jout) throws IOException
2149      {
2150          JarDiff.createPatch(ofile.getPath(), nfile.getPath(), jout, false);
2151      }
2152 @@ -209,7 +207,7 @@ public class Differ
2153          Differ differ = new Differ();
2154          boolean verbose = false;
2155          int aidx = 0;
2156 -        if (args[0].equals("-verbose")) {
2157 +        if ("-verbose".equals(args[0])) {
2158              verbose = true;
2159              aidx++;
2160          }
2161 @@ -222,11 +220,10 @@ public class Differ
2162          }
2163      }
2164  
2165 -    protected static void pipe (File file, JarOutputStream jout)
2166 -        throws IOException
2167 +    protected static void pipe (File file, OutputStream out) throws IOException
2168      {
2169          try (FileInputStream fin = new FileInputStream(file)) {
2170 -            StreamUtil.copy(fin, jout);
2171 +            StreamUtil.copy(fin, out);
2172          }
2173      }
2174  }
2175 diff --git a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/tools/Digester.java b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/tools/Digester.java
2176 index b04a6539b..ae61b4333 100644
2177 --- a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/tools/Digester.java
2178 +++ b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/tools/Digester.java
2179 @@ -23,6 +23,7 @@ import com.threerings.getdown.data.Digest;
2180  import com.threerings.getdown.data.EnvConfig;
2181  import com.threerings.getdown.data.Resource;
2182  import com.threerings.getdown.util.Base64;
2183 +import com.threerings.getdown.util.Config;
2184  
2185  import static java.nio.charset.StandardCharsets.UTF_8;
2186  
2187 @@ -74,8 +75,11 @@ public class Digester
2188          System.out.println("Generating digest file '" + target + "'...");
2189  
2190          // create our application and instruct it to parse its business
2191 -        Application app = new Application(new EnvConfig(appdir));
2192 -        app.init(false);
2193 +        EnvConfig envc = new EnvConfig(appdir);
2194 +        Application app = new Application(envc);
2195 +        Config config = Application.readConfig(envc, false);
2196 +        app.initBase(config);
2197 +        app.initResources(config);
2198  
2199          List<Resource> rsrcs = new ArrayList<>();
2200          rsrcs.add(app.getConfigResource());
2201 @@ -86,6 +90,9 @@ public class Digester
2202              rsrcs.addAll(ag.rsrcs);
2203          }
2204  
2205 +        // reinit app just to verify that getdown.txt has valid format
2206 +        app.init(true);
2207 +
2208          // now generate the digest file
2209          Digest.createDigest(version, rsrcs, target);
2210      }
2211 diff --git a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/tools/JarDiff.java b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/tools/JarDiff.java
2212 index 1cea0eacd..f0db8ac03 100644
2213 --- a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/tools/JarDiff.java
2214 +++ b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/tools/JarDiff.java
2215 @@ -41,15 +41,24 @@
2216  
2217  package com.threerings.getdown.tools;
2218  
2219 -import java.io.*;
2220 +import java.io.Closeable;
2221 +import java.io.File;
2222 +import java.io.IOException;
2223 +import java.io.InputStream;
2224 +import java.io.OutputStream;
2225 +import java.io.StringWriter;
2226 +import java.io.Writer;
2227  import java.util.*;
2228 -import java.util.jar.*;
2229 +import java.util.zip.ZipEntry;
2230 +import java.util.zip.ZipFile;
2231 +import java.util.zip.ZipOutputStream;
2232  
2233  import static java.nio.charset.StandardCharsets.UTF_8;
2234  
2235  /**
2236 - * JarDiff is able to create a jar file containing the delta between two jar files (old and new).
2237 - * The delta jar file can then be applied to the old jar file to reconstruct the new jar file.
2238 + * JarDiff is able to create a zip file containing the delta between two jar or zip files (old
2239 + * and new). The delta file can then be applied to the old archive file to reconstruct the new
2240 + * archive file.
2241   *
2242   * <p> Refer to the JNLP spec for details on how this is done.
2243   *
2244 @@ -58,39 +67,37 @@ import static java.nio.charset.StandardCharsets.UTF_8;
2245  public class JarDiff implements JarDiffCodes
2246  {
2247      private static final int DEFAULT_READ_SIZE = 2048;
2248 -    private static byte[] newBytes = new byte[DEFAULT_READ_SIZE];
2249 -    private static byte[] oldBytes = new byte[DEFAULT_READ_SIZE];
2250 +    private static final byte[] newBytes = new byte[DEFAULT_READ_SIZE];
2251 +    private static final byte[] oldBytes = new byte[DEFAULT_READ_SIZE];
2252  
2253      // The JARDiff.java is the stand-alone jardiff.jar tool. Thus, we do not depend on Globals.java
2254      // and other stuff here. Instead, we use an explicit _debug flag.
2255      private static boolean _debug;
2256  
2257      /**
2258 -     * Creates a patch from the two passed in files, writing the result to <code>os</code>.
2259 +     * Creates a patch from the two passed in files, writing the result to {@code os}.
2260       */
2261      public static void createPatch (String oldPath, String newPath,
2262                                      OutputStream os, boolean minimal) throws IOException
2263      {
2264 -        try (JarFile2 oldJar = new JarFile2(oldPath);
2265 -             JarFile2 newJar = new JarFile2(newPath)) {
2266 +        try (ZipFile2 oldArchive = new ZipFile2(oldPath);
2267 +             ZipFile2 newArchive = new ZipFile2(newPath)) {
2268  
2269 -            HashMap<String,String> moved = new HashMap<>();
2270 -            HashSet<String> implicit = new HashSet<>();
2271 -            HashSet<String> moveSrc = new HashSet<>();
2272 -            HashSet<String> newEntries = new HashSet<>();
2273 +            Map<String,String> moved = new HashMap<>();
2274 +            Set<String> implicit = new HashSet<>();
2275 +            Set<String> moveSrc = new HashSet<>();
2276 +            Set<String> newEntries = new HashSet<>();
2277  
2278              // FIRST PASS
2279 -            // Go through the entries in new jar and
2280 -            // determine which files are candidates for implicit moves
2281 -            // ( files that has the same filename and same content in old.jar
2282 -            // and new.jar )
2283 -            // and for files that cannot be implicitly moved, we will either
2284 -            // find out whether it is moved or new (modified)
2285 -            for (JarEntry newEntry : newJar) {
2286 +            // Go through the entries in new archive and determine which files are candidates for
2287 +            // implicit moves (files that have the same filename and same content in old and new)
2288 +            // and for files that cannot be implicitly moved, we will either find out whether it is
2289 +            // moved or new (modified)
2290 +            for (ZipEntry newEntry : newArchive) {
2291                  String newname = newEntry.getName();
2292  
2293                  // Return best match of contents, will return a name match if possible
2294 -                String oldname = oldJar.getBestMatch(newJar, newEntry);
2295 +                String oldname = oldArchive.getBestMatch(newArchive, newEntry);
2296                  if (oldname == null) {
2297                      // New or modified entry
2298                      if (_debug) {
2299 @@ -101,7 +108,7 @@ public class JarDiff implements JarDiffCodes
2300                      // Content already exist - need to do a move
2301  
2302                      // Should do implicit move? Yes, if names are the same, and
2303 -                    // no move command already exist from oldJar
2304 +                    // no move command already exist from oldArchive
2305                      if (oldname.equals(newname) && !moveSrc.contains(oldname)) {
2306                          if (_debug) {
2307                              System.out.println(newname + " added to implicit set!");
2308 @@ -117,12 +124,9 @@ public class JarDiff implements JarDiffCodes
2309                          // JarDiffPatcher also.
2310                          if (!minimal && (implicit.contains(oldname) ||
2311                                           moveSrc.contains(oldname) )) {
2312 -
2313                              // generate non-minimal jardiff
2314                              // for backward compatibility
2315 -
2316                              if (_debug) {
2317 -
2318                                  System.out.println("NEW: "+ newname);
2319                              }
2320                              newEntries.add(newname);
2321 @@ -153,8 +157,8 @@ public class JarDiff implements JarDiffCodes
2322  
2323              // SECOND PASS: <deleted files> = <oldjarnames> - <implicitmoves> -
2324              // <source of move commands> - <new or modified entries>
2325 -            ArrayList<String> deleted = new ArrayList<>();
2326 -            for (JarEntry oldEntry : oldJar) {
2327 +            List<String> deleted = new ArrayList<>();
2328 +            for (ZipEntry oldEntry : oldArchive) {
2329                  String oldName = oldEntry.getName();
2330                  if (!implicit.contains(oldName) && !moveSrc.contains(oldName)
2331                      && !newEntries.contains(oldName)) {
2332 @@ -180,7 +184,7 @@ public class JarDiff implements JarDiffCodes
2333                  }
2334              }
2335  
2336 -            JarOutputStream jos = new JarOutputStream(os);
2337 +            ZipOutputStream jos = new ZipOutputStream(os);
2338  
2339              // Write out all the MOVEs and REMOVEs
2340              createIndex(jos, deleted, moved);
2341 @@ -190,7 +194,7 @@ public class JarDiff implements JarDiffCodes
2342                  if (_debug) {
2343                      System.out.println("New File: " + newName);
2344                  }
2345 -                writeEntry(jos, newJar.getEntryByName(newName), newJar);
2346 +                writeEntry(jos, newArchive.getEntryByName(newName), newArchive);
2347              }
2348  
2349              jos.finish();
2350 @@ -199,11 +203,11 @@ public class JarDiff implements JarDiffCodes
2351      }
2352  
2353      /**
2354 -     * Writes the index file out to <code>jos</code>.
2355 -     * <code>oldEntries</code> gives the names of the files that were removed,
2356 -     * <code>movedMap</code> maps from the new name to the old name.
2357 +     * Writes the index file out to {@code jos}.
2358 +     * {@code oldEntries} gives the names of the files that were removed,
2359 +     * {@code movedMap} maps from the new name to the old name.
2360       */
2361 -    private static void createIndex (JarOutputStream jos, List<String> oldEntries,
2362 +    private static void createIndex (ZipOutputStream jos, List<String> oldEntries,
2363                                       Map<String,String> movedMap)
2364          throws IOException
2365      {
2366 @@ -220,17 +224,17 @@ public class JarDiff implements JarDiffCodes
2367          }
2368  
2369          // And those that have moved
2370 -        for (String newName : movedMap.keySet()) {
2371 -            String oldName = movedMap.get(newName);
2372 +        for (Map.Entry<String, String> entry : movedMap.entrySet()) {
2373 +            String oldName = entry.getValue();
2374              writer.write(MOVE_COMMAND);
2375              writer.write(" ");
2376              writeEscapedString(writer, oldName);
2377              writer.write(" ");
2378 -            writeEscapedString(writer, newName);
2379 +            writeEscapedString(writer, entry.getKey());
2380              writer.write("\r\n");
2381          }
2382  
2383 -        jos.putNextEntry(new JarEntry(INDEX_NAME));
2384 +        jos.putNextEntry(new ZipEntry(INDEX_NAME));
2385          byte[] bytes = writer.toString().getBytes(UTF_8);
2386          jos.write(bytes, 0, bytes.length);
2387      }
2388 @@ -264,10 +268,10 @@ public class JarDiff implements JarDiffCodes
2389          return writer;
2390      }
2391  
2392 -    private static void writeEntry (JarOutputStream jos, JarEntry entry, JarFile2 file)
2393 +    private static void writeEntry (ZipOutputStream jos, ZipEntry entry, ZipFile2 file)
2394          throws IOException
2395      {
2396 -        try (InputStream data = file.getJarFile().getInputStream(entry)) {
2397 +        try (InputStream data = file.getArchive().getInputStream(entry)) {
2398              jos.putNextEntry(entry);
2399              int size = data.read(newBytes);
2400              while (size != -1) {
2401 @@ -278,31 +282,31 @@ public class JarDiff implements JarDiffCodes
2402      }
2403  
2404      /**
2405 -     * JarFile2 wraps a JarFile providing some convenience methods.
2406 +     * ZipFile2 wraps a ZipFile providing some convenience methods.
2407       */
2408 -    private static class JarFile2 implements Iterable<JarEntry>, Closeable
2409 +    private static class ZipFile2 implements Iterable<ZipEntry>, Closeable
2410      {
2411 -        private JarFile _jar;
2412 -        private List<JarEntry> _entries;
2413 -        private HashMap<String,JarEntry> _nameToEntryMap;
2414 -        private HashMap<Long,LinkedList<JarEntry>> _crcToEntryMap;
2415 +        private final ZipFile _archive;
2416 +        private List<ZipEntry> _entries;
2417 +        private HashMap<String,ZipEntry> _nameToEntryMap;
2418 +        private HashMap<Long,LinkedList<ZipEntry>> _crcToEntryMap;
2419  
2420 -        public JarFile2 (String path) throws IOException {
2421 -            _jar = new JarFile(new File(path));
2422 +        public ZipFile2 (String path) throws IOException {
2423 +            _archive = new ZipFile(new File(path));
2424              index();
2425          }
2426  
2427 -        public JarFile getJarFile () {
2428 -            return _jar;
2429 +        public ZipFile getArchive () {
2430 +            return _archive;
2431          }
2432  
2433 -        // from interface Iterable<JarEntry>
2434 +        // from interface Iterable<ZipEntry>
2435          @Override
2436 -        public Iterator<JarEntry> iterator () {
2437 +        public Iterator<ZipEntry> iterator () {
2438              return _entries.iterator();
2439          }
2440  
2441 -        public JarEntry getEntryByName (String name) {
2442 +        public ZipEntry getEntryByName (String name) {
2443              return _nameToEntryMap.get(name);
2444          }
2445  
2446 @@ -350,7 +354,7 @@ public class JarDiff implements JarDiffCodes
2447              return retVal;
2448          }
2449  
2450 -        public String getBestMatch (JarFile2 file, JarEntry entry) throws IOException {
2451 +        public String getBestMatch (ZipFile2 file, ZipEntry entry) throws IOException {
2452              // check for same name and same content, return name if found
2453              if (contains(file, entry)) {
2454                  return (entry.getName());
2455 @@ -360,11 +364,10 @@ public class JarDiff implements JarDiffCodes
2456              return (hasSameContent(file,entry));
2457          }
2458  
2459 -        public boolean contains (JarFile2 f, JarEntry e) throws IOException {
2460 -
2461 -            JarEntry thisEntry = getEntryByName(e.getName());
2462 +        public boolean contains (ZipFile2 f, ZipEntry e) throws IOException {
2463 +            ZipEntry thisEntry = getEntryByName(e.getName());
2464  
2465 -            // Look up name in 'this' Jar2File - if not exist return false
2466 +            // Look up name in 'this' ZipFile2 - if not exist return false
2467              if (thisEntry == null)
2468                  return false;
2469  
2470 @@ -373,26 +376,26 @@ public class JarDiff implements JarDiffCodes
2471                  return false;
2472  
2473              // Check contents - if no match - return false
2474 -            try (InputStream oldIS = getJarFile().getInputStream(thisEntry);
2475 -                 InputStream newIS = f.getJarFile().getInputStream(e)) {
2476 +            try (InputStream oldIS = getArchive().getInputStream(thisEntry);
2477 +                 InputStream newIS = f.getArchive().getInputStream(e)) {
2478                  return !differs(oldIS, newIS);
2479              }
2480          }
2481  
2482 -        public String hasSameContent (JarFile2 file, JarEntry entry) throws IOException {
2483 +        public String hasSameContent (ZipFile2 file, ZipEntry entry) throws IOException {
2484              String thisName = null;
2485 -            Long crcL = Long.valueOf(entry.getCrc());
2486 -            // check if this jar contains files with the passed in entry's crc
2487 +            Long crcL = entry.getCrc();
2488 +            // check if this archive contains files with the passed in entry's crc
2489              if (_crcToEntryMap.containsKey(crcL)) {
2490                  // get the Linked List with files with the crc
2491 -                LinkedList<JarEntry> ll = _crcToEntryMap.get(crcL);
2492 +                LinkedList<ZipEntry> ll = _crcToEntryMap.get(crcL);
2493                  // go through the list and check for content match
2494 -                ListIterator<JarEntry> li = ll.listIterator(0);
2495 +                ListIterator<ZipEntry> li = ll.listIterator(0);
2496                  while (li.hasNext()) {
2497 -                    JarEntry thisEntry = li.next();
2498 +                    ZipEntry thisEntry = li.next();
2499                      // check for content match
2500 -                    try (InputStream oldIS = getJarFile().getInputStream(thisEntry);
2501 -                         InputStream newIS = file.getJarFile().getInputStream(entry)) {
2502 +                    try (InputStream oldIS = getArchive().getInputStream(thisEntry);
2503 +                         InputStream newIS = file.getArchive().getInputStream(entry)) {
2504                          if (!differs(oldIS, newIS)) {
2505                              thisName = thisEntry.getName();
2506                              return thisName;
2507 @@ -404,19 +407,19 @@ public class JarDiff implements JarDiffCodes
2508          }
2509  
2510          private void index () throws IOException {
2511 -            Enumeration<JarEntry> entries = _jar.entries();
2512 +            Enumeration<? extends ZipEntry> entries = _archive.entries();
2513  
2514              _nameToEntryMap = new HashMap<>();
2515              _crcToEntryMap = new HashMap<>();
2516              _entries = new ArrayList<>();
2517              if (_debug) {
2518 -                System.out.println("indexing: " + _jar.getName());
2519 +                System.out.println("indexing: " + _archive.getName());
2520              }
2521              if (entries != null) {
2522                  while (entries.hasMoreElements()) {
2523 -                    JarEntry entry = entries.nextElement();
2524 +                    ZipEntry entry = entries.nextElement();
2525                      long crc = entry.getCrc();
2526 -                    Long crcL = Long.valueOf(crc);
2527 +                    Long crcL = crc;
2528                      if (_debug) {
2529                          System.out.println("\t" + entry.getName() + " CRC " + crc);
2530                      }
2531 @@ -427,13 +430,13 @@ public class JarDiff implements JarDiffCodes
2532                      // generate the CRC to entries map
2533                      if (_crcToEntryMap.containsKey(crcL)) {
2534                          // key exist, add the entry to the correcponding linked list
2535 -                        LinkedList<JarEntry> ll = _crcToEntryMap.get(crcL);
2536 +                        LinkedList<ZipEntry> ll = _crcToEntryMap.get(crcL);
2537                          ll.add(entry);
2538                          _crcToEntryMap.put(crcL, ll);
2539  
2540                      } else {
2541                          // create a new entry in the hashmap for the new key
2542 -                        LinkedList<JarEntry> ll = new LinkedList<JarEntry>();
2543 +                        LinkedList<ZipEntry> ll = new LinkedList<>();
2544                          ll.add(entry);
2545                          _crcToEntryMap.put(crcL, ll);
2546                      }
2547 @@ -443,7 +446,7 @@ public class JarDiff implements JarDiffCodes
2548  
2549          @Override
2550          public void close() throws IOException {
2551 -            _jar.close();
2552 +            _archive.close();
2553          }
2554      }
2555  }
2556 diff --git a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/tools/JarDiffPatcher.java b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/tools/JarDiffPatcher.java
2557 index b5a0a1763..e55034bca 100644
2558 --- a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/tools/JarDiffPatcher.java
2559 +++ b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/tools/JarDiffPatcher.java
2560 @@ -16,27 +16,25 @@ import java.util.ArrayList;
2561  import java.util.Enumeration;
2562  import java.util.HashMap;
2563  import java.util.HashSet;
2564 -import java.util.Iterator;
2565  import java.util.List;
2566  import java.util.Map;
2567  import java.util.Set;
2568 -
2569 -import java.util.jar.JarEntry;
2570 -import java.util.jar.JarFile;
2571  import java.util.jar.JarOutputStream;
2572 +import java.util.zip.ZipEntry;
2573 +import java.util.zip.ZipFile;
2574 +import java.util.zip.ZipOutputStream;
2575  
2576  import com.threerings.getdown.util.ProgressObserver;
2577 -
2578  import static java.nio.charset.StandardCharsets.UTF_8;
2579  
2580  /**
2581 - * Applies a jardiff patch to a jar file.
2582 + * Applies a jardiff patch to a jar/zip file.
2583   */
2584  public class JarDiffPatcher implements JarDiffCodes
2585  {
2586      /**
2587 -     * Patches the specified jar file using the supplied patch file and writing
2588 -     * the new jar file to the supplied target.
2589 +     * Patches the specified jar file using the supplied patch file and writing the new jar file to
2590 +     * the supplied target.
2591       *
2592       * @param jarPath the path to the original jar file.
2593       * @param diffPath the path to the jardiff patch file.
2594 @@ -49,11 +47,9 @@ public class JarDiffPatcher implements JarDiffCodes
2595          throws IOException
2596      {
2597          File oldFile = new File(jarPath), diffFile = new File(diffPath);
2598 -
2599 -        try (JarFile oldJar = new JarFile(oldFile);
2600 -             JarFile jarDiff = new JarFile(diffFile);
2601 -             JarOutputStream jos = new JarOutputStream(new FileOutputStream(target))) {
2602 -
2603 +        try (ZipFile oldJar = new ZipFile(oldFile);
2604 +             ZipFile jarDiff = new ZipFile(diffFile);
2605 +             ZipOutputStream jos = makeOutputStream(oldFile, target)) {
2606              Set<String> ignoreSet = new HashSet<>();
2607              Map<String, String> renameMap = new HashMap<>();
2608              determineNameMapping(jarDiff, ignoreSet, renameMap);
2609 @@ -63,7 +59,7 @@ public class JarDiffPatcher implements JarDiffCodes
2610  
2611              // Files to implicit move
2612              Set<String> oldjarNames  = new HashSet<>();
2613 -            Enumeration<JarEntry> oldEntries = oldJar.entries();
2614 +            Enumeration<? extends ZipEntry> oldEntries = oldJar.entries();
2615              if (oldEntries != null) {
2616                  while  (oldEntries.hasMoreElements()) {
2617                      oldjarNames.add(oldEntries.nextElement().getName());
2618 @@ -83,10 +79,10 @@ public class JarDiffPatcher implements JarDiffCodes
2619              size -= ignoreSet.size();
2620  
2621              // Add content from JARDiff
2622 -            Enumeration<JarEntry> entries = jarDiff.entries();
2623 +            Enumeration<? extends ZipEntry> entries = jarDiff.entries();
2624              if (entries != null) {
2625                  while (entries.hasMoreElements()) {
2626 -                    JarEntry entry = entries.nextElement();
2627 +                    ZipEntry entry = entries.nextElement();
2628                      if (!INDEX_NAME.equals(entry.getName())) {
2629                          updateObserver(observer, currentEntry, size);
2630                          currentEntry++;
2631 @@ -114,15 +110,15 @@ public class JarDiffPatcher implements JarDiffCodes
2632                  // Apply move <oldName> <newName> command
2633                  String oldName = renameMap.get(newName);
2634  
2635 -                // Get source JarEntry
2636 -                JarEntry oldEntry = oldJar.getJarEntry(oldName);
2637 +                // Get source ZipEntry
2638 +                ZipEntry oldEntry = oldJar.getEntry(oldName);
2639                  if (oldEntry == null) {
2640                      String moveCmd = MOVE_COMMAND + oldName + " " + newName;
2641                      throw new IOException("error.badmove: " + moveCmd);
2642                  }
2643  
2644 -                // Create dest JarEntry
2645 -                JarEntry newEntry = new JarEntry(newName);
2646 +                // Create dest ZipEntry
2647 +                ZipEntry newEntry = new ZipEntry(newName);
2648                  newEntry.setTime(oldEntry.getTime());
2649                  newEntry.setSize(oldEntry.getSize());
2650                  newEntry.setCompressedSize(oldEntry.getCompressedSize());
2651 @@ -149,19 +145,15 @@ public class JarDiffPatcher implements JarDiffCodes
2652              }
2653  
2654              // implicit move
2655 -            Iterator<String> iEntries = oldjarNames.iterator();
2656 -            if (iEntries != null) {
2657 -                while (iEntries.hasNext()) {
2658 -                    String name = iEntries.next();
2659 -                    JarEntry entry = oldJar.getJarEntry(name);
2660 -                    if (entry == null) {
2661 -                        // names originally retrieved from the JAR, so this should never happen
2662 -                        throw new AssertionError("JAR entry not found: " + name);
2663 -                    }
2664 -                    updateObserver(observer, currentEntry, size);
2665 -                    currentEntry++;
2666 -                    writeEntry(jos, entry, oldJar);
2667 +            for (String name : oldjarNames) {
2668 +                ZipEntry entry = oldJar.getEntry(name);
2669 +                if (entry == null) {
2670 +                    // names originally retrieved from the archive, so this should never happen
2671 +                    throw new AssertionError("Archive entry not found: " + name);
2672                  }
2673 +                updateObserver(observer, currentEntry, size);
2674 +                currentEntry++;
2675 +                writeEntry(jos, entry, oldJar);
2676              }
2677              updateObserver(observer, currentEntry, size);
2678          }
2679 @@ -175,7 +167,7 @@ public class JarDiffPatcher implements JarDiffCodes
2680      }
2681  
2682      protected void determineNameMapping (
2683 -        JarFile jarDiff, Set<String> ignoreSet, Map<String, String> renameMap)
2684 +        ZipFile jarDiff, Set<String> ignoreSet, Map<String, String> renameMap)
2685          throws IOException
2686      {
2687          InputStream is = jarDiff.getInputStream(jarDiff.getEntry(INDEX_NAME));
2688 @@ -223,7 +215,7 @@ public class JarDiffPatcher implements JarDiffCodes
2689      {
2690          int index = 0;
2691          int length = path.length();
2692 -        ArrayList<String> sub = new ArrayList<>();
2693 +        List<String> sub = new ArrayList<>();
2694  
2695          while (index < length) {
2696              while (index < length && Character.isWhitespace
2697 @@ -231,9 +223,8 @@ public class JarDiffPatcher implements JarDiffCodes
2698                  index++;
2699              }
2700              if (index < length) {
2701 -                int start = index;
2702 -                int last = start;
2703 -                String subString = null;
2704 +                int last = index;
2705 +                StringBuilder subString = null;
2706  
2707                  while (index < length) {
2708                      char aChar = path.charAt(index);
2709 @@ -241,9 +232,9 @@ public class JarDiffPatcher implements JarDiffCodes
2710                          path.charAt(index + 1) == ' ') {
2711  
2712                          if (subString == null) {
2713 -                            subString = path.substring(last, index);
2714 +                            subString = new StringBuilder(path.substring(last, index));
2715                          } else {
2716 -                            subString += path.substring(last, index);
2717 +                            subString.append(path, last, index);
2718                          }
2719                          last = ++index;
2720                      } else if (Character.isWhitespace(aChar)) {
2721 @@ -253,18 +244,20 @@ public class JarDiffPatcher implements JarDiffCodes
2722                  }
2723                  if (last != index) {
2724                      if (subString == null) {
2725 -                        subString = path.substring(last, index);
2726 +                        subString = new StringBuilder(path.substring(last, index));
2727                      } else {
2728 -                        subString += path.substring(last, index);
2729 +                        subString.append(path, last, index);
2730                      }
2731                  }
2732 -                sub.add(subString);
2733 +                if (subString != null) {
2734 +                    sub.add(subString.toString());
2735 +                }
2736              }
2737          }
2738          return sub;
2739      }
2740  
2741 -    protected void writeEntry (JarOutputStream jos, JarEntry entry, JarFile file)
2742 +    protected void writeEntry (ZipOutputStream jos, ZipEntry entry, ZipFile file)
2743          throws IOException
2744      {
2745          try (InputStream data = file.getInputStream(entry)) {
2746 @@ -272,10 +265,10 @@ public class JarDiffPatcher implements JarDiffCodes
2747          }
2748      }
2749  
2750 -    protected void writeEntry (JarOutputStream jos, JarEntry entry, InputStream data)
2751 +    protected void writeEntry (ZipOutputStream jos, ZipEntry entry, InputStream data)
2752          throws IOException
2753      {
2754 -        jos.putNextEntry(new JarEntry(entry.getName()));
2755 +        jos.putNextEntry(new ZipEntry(entry.getName()));
2756  
2757          // Read the entry
2758          int size = data.read(newBytes);
2759 @@ -285,6 +278,15 @@ public class JarDiffPatcher implements JarDiffCodes
2760          }
2761      }
2762  
2763 +    protected static ZipOutputStream makeOutputStream (File source, File target)
2764 +        throws IOException
2765 +    {
2766 +        FileOutputStream out = new FileOutputStream(target);
2767 +        if (source.getName().endsWith(".jar")) return new JarOutputStream(out);
2768 +        else if (source.getName().endsWith(".zip")) return new ZipOutputStream(out);
2769 +        else throw new AssertionError("Unsupported source file '" + source + "'. Not a .jar or .zip?");
2770 +    }
2771 +
2772      protected static final int DEFAULT_READ_SIZE = 2048;
2773  
2774      protected static byte[] newBytes = new byte[DEFAULT_READ_SIZE];
2775 diff --git a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/tools/Patcher.java b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/tools/Patcher.java
2776 index 4ead59bb3..5c8baaaab 100644
2777 --- a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/tools/Patcher.java
2778 +++ b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/tools/Patcher.java
2779 @@ -9,16 +9,13 @@ import java.io.File;
2780  import java.io.FileOutputStream;
2781  import java.io.IOException;
2782  import java.io.InputStream;
2783 -
2784  import java.util.Enumeration;
2785 -import java.util.jar.JarEntry;
2786 -import java.util.jar.JarFile;
2787  import java.util.zip.ZipEntry;
2788 +import java.util.zip.ZipFile;
2789  
2790  import com.threerings.getdown.util.FileUtil;
2791  import com.threerings.getdown.util.ProgressObserver;
2792  import com.threerings.getdown.util.StreamUtil;
2793 -
2794  import static com.threerings.getdown.Log.log;
2795  
2796  /**
2797 @@ -55,10 +52,10 @@ public class Patcher
2798          _obs = obs;
2799          _plength = patch.length();
2800  
2801 -        try (JarFile file = new JarFile(patch)) {
2802 -            Enumeration<JarEntry> entries = file.entries(); // old skool!
2803 +        try (ZipFile file = new ZipFile(patch)) {
2804 +            Enumeration<? extends ZipEntry> entries = file.entries();
2805              while (entries.hasMoreElements()) {
2806 -                JarEntry entry = entries.nextElement();
2807 +                ZipEntry entry = entries.nextElement();
2808                  String path = entry.getName();
2809                  long elength = entry.getCompressedSize();
2810  
2811 @@ -96,7 +93,7 @@ public class Patcher
2812          return path.substring(0, path.length() - suffix.length());
2813      }
2814  
2815 -    protected void createFile (JarFile file, ZipEntry entry, File target)
2816 +    protected void createFile (ZipFile file, ZipEntry entry, File target)
2817      {
2818          // create our copy buffer if necessary
2819          if (_buffer == null) {
2820 @@ -124,8 +121,7 @@ public class Patcher
2821          }
2822      }
2823  
2824 -    protected void patchFile (JarFile file, ZipEntry entry,
2825 -                              File appdir, String path)
2826 +    protected void patchFile (ZipFile file, ZipEntry entry, File appdir, String path)
2827      {
2828          File target = new File(appdir, path);
2829          File patch = new File(appdir, entry.getName());
2830 diff --git a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/util/Base64.java b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/util/Base64.java
2831 index 2a5db79ec..233f1e739 100644
2832 --- a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/util/Base64.java
2833 +++ b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/util/Base64.java
2834 @@ -24,7 +24,7 @@ import static java.nio.charset.StandardCharsets.US_ASCII;
2835   * href="http://www.ietf.org/rfc/rfc2045.txt">2045</a> and <a
2836   * href="http://www.ietf.org/rfc/rfc3548.txt">3548</a>.
2837   */
2838 -public class Base64 {
2839 +public final class Base64 {
2840      /**
2841       * Default values for encoder/decoder flags.
2842       */
2843 @@ -68,7 +68,7 @@ public class Base64 {
2844      //  shared code
2845      //  --------------------------------------------------------
2846  
2847 -    /* package */ static abstract class Coder {
2848 +    /* package */ abstract static class Coder {
2849          public byte[] output;
2850          public int op;
2851  
2852 @@ -178,7 +178,7 @@ public class Base64 {
2853           * Lookup table for turning bytes into their position in the
2854           * Base64 alphabet.
2855           */
2856 -        private static final int DECODE[] = {
2857 +        private static final int[] DECODE = {
2858              -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
2859              -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
2860              -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1, -1, 63,
2861 @@ -201,7 +201,7 @@ public class Base64 {
2862           * Decode lookup table for the "web safe" variant (RFC 3548
2863           * sec. 4) where - and _ replace + and /.
2864           */
2865 -        private static final int DECODE_WEBSAFE[] = {
2866 +        private static final int[] DECODE_WEBSAFE = {
2867              -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
2868              -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
2869              -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1,
2870 @@ -236,7 +236,7 @@ public class Base64 {
2871          private int state;   // state number (0 to 6)
2872          private int value;
2873  
2874 -        final private int[] alphabet;
2875 +        private final int[] alphabet;
2876  
2877          public Decoder(int flags, byte[] output) {
2878              this.output = output;
2879 @@ -541,7 +541,7 @@ public class Base64 {
2880           * Lookup table for turning Base64 alphabet positions (6 bits)
2881           * into output bytes.
2882           */
2883 -        private static final byte ENCODE[] = {
2884 +        private static final byte[] ENCODE = {
2885              'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P',
2886              'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f',
2887              'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v',
2888 @@ -552,21 +552,21 @@ public class Base64 {
2889           * Lookup table for turning Base64 alphabet positions (6 bits)
2890           * into output bytes.
2891           */
2892 -        private static final byte ENCODE_WEBSAFE[] = {
2893 +        private static final byte[] ENCODE_WEBSAFE = {
2894              'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P',
2895              'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f',
2896              'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v',
2897              'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '-', '_',
2898          };
2899  
2900 -        final private byte[] tail;
2901 +        private final byte[] tail;
2902          /* package */ int tailLen;
2903          private int count;
2904  
2905 -        final public boolean do_padding;
2906 -        final public boolean do_newline;
2907 -        final public boolean do_cr;
2908 -        final private byte[] alphabet;
2909 +        public final boolean do_padding;
2910 +        public final boolean do_newline;
2911 +        public final boolean do_cr;
2912 +        private final byte[] alphabet;
2913  
2914          public Encoder(int flags, byte[] output) {
2915              this.output = output;
2916 @@ -618,7 +618,7 @@ public class Base64 {
2917                              ((input[p++] & 0xff) << 8) |
2918                              (input[p++] & 0xff);
2919                          tailLen = 0;
2920 -                    };
2921 +                    }
2922                      break;
2923  
2924                  case 2:
2925 diff --git a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/util/Color.java b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/util/Color.java
2926 index 047cead76..416f77e58 100644
2927 --- a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/util/Color.java
2928 +++ b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/util/Color.java
2929 @@ -8,11 +8,11 @@ package com.threerings.getdown.util;
2930  /**
2931   * Utilities for handling ARGB colors.
2932   */
2933 -public class Color
2934 +public final class Color
2935  {
2936 -    public final static int CLEAR = 0x00000000;
2937 -    public final static int WHITE = 0xFFFFFFFF;
2938 -    public final static int BLACK = 0xFF000000;
2939 +    public static final int CLEAR = 0x00000000;
2940 +    public static final int WHITE = 0xFFFFFFFF;
2941 +    public static final int BLACK = 0xFF000000;
2942  
2943      public static float brightness (int argb) {
2944          // TODO: we're ignoring alpha here...
2945 diff --git a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/util/Config.java b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/util/Config.java
2946 index 6ad2b4fd9..75357e882 100644
2947 --- a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/util/Config.java
2948 +++ b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/util/Config.java
2949 @@ -14,7 +14,6 @@ import java.net.MalformedURLException;
2950  import java.net.URL;
2951  import java.nio.charset.StandardCharsets;
2952  import java.util.ArrayList;
2953 -import java.util.Arrays;
2954  import java.util.HashMap;
2955  import java.util.List;
2956  import java.util.Locale;
2957 @@ -58,33 +57,17 @@ public class Config
2958      }
2959  
2960      /**
2961 -     * Parses a configuration file containing key/value pairs. The file must be in the UTF-8
2962 -     * encoding.
2963 -     *
2964 +     * Parses configuration text containing key/value pairs.
2965       * @param opts options that influence the parsing. See {@link #createOpts}.
2966 -     *
2967 -     * @return a list of <code>String[]</code> instances containing the key/value pairs in the
2968 +     * @return a list of {@code String[]} instances containing the key/value pairs in the
2969       * order they were parsed from the file.
2970       */
2971 -    public static List<String[]> parsePairs (File source, ParseOpts opts)
2972 -        throws IOException
2973 -    {
2974 -        // annoyingly FileReader does not allow encoding to be specified (uses platform default)
2975 -        try (FileInputStream fis = new FileInputStream(source);
2976 -             InputStreamReader input = new InputStreamReader(fis, StandardCharsets.UTF_8)) {
2977 -            return parsePairs(input, opts);
2978 -        }
2979 -    }
2980 -
2981 -    /**
2982 -     * See {@link #parsePairs(File,ParseOpts)}.
2983 -     */
2984      public static List<String[]> parsePairs (Reader source, ParseOpts opts) throws IOException
2985      {
2986          List<String[]> pairs = new ArrayList<>();
2987          for (String line : FileUtil.readLines(source)) {
2988              // nix comments
2989 -            int cidx = line.indexOf("#");
2990 +            int cidx = line.indexOf('#');
2991              if (opts.strictComments ? cidx == 0 : cidx != -1) {
2992                  line = line.substring(0, cidx);
2993              }
2994 @@ -98,7 +81,7 @@ public class Config
2995              // parse our key/value pair
2996              String[] pair = new String[2];
2997              // if we're biasing toward key, put all the extra = in the key rather than the value
2998 -            int eidx = opts.biasToKey ? line.lastIndexOf("=") : line.indexOf("=");
2999 +            int eidx = opts.biasToKey ? line.lastIndexOf('=') : line.indexOf('=');
3000              if (eidx != -1) {
3001                  pair[0] = line.substring(0, eidx).trim();
3002                  pair[1] = line.substring(eidx+1).trim();
3003 @@ -109,7 +92,7 @@ public class Config
3004  
3005              // if the pair has an os qualifier, we need to process it
3006              if (pair[1].startsWith("[")) {
3007 -                int qidx = pair[1].indexOf("]");
3008 +                int qidx = pair[1].indexOf(']');
3009                  if (qidx == -1) {
3010                      log.warning("Bogus platform specifier", "key", pair[0], "value", pair[1]);
3011                      continue; // omit the pair entirely
3012 @@ -132,6 +115,21 @@ public class Config
3013          return pairs;
3014      }
3015  
3016 +    /**
3017 +     * Parses configuration file containing key/value pairs.
3018 +     * @param source the file containing the config text. Must be in the UTF-8 encoding.
3019 +     * @param opts options that influence the parsing. See {@link #createOpts}.
3020 +     */
3021 +    public static List<String[]> parsePairs (File source, ParseOpts opts)
3022 +        throws IOException
3023 +    {
3024 +        // annoyingly FileReader does not allow encoding to be specified (uses platform default)
3025 +        try (FileInputStream fis = new FileInputStream(source);
3026 +             InputStreamReader input = new InputStreamReader(fis, StandardCharsets.UTF_8)) {
3027 +            return parsePairs(input, opts);
3028 +        }
3029 +    }
3030 +
3031      /**
3032       * Takes a comma-separated String of four integers and returns a rectangle using those ints as
3033       * the its x, y, width, and height.
3034 @@ -167,13 +165,10 @@ public class Config
3035      }
3036  
3037      /**
3038 -     * Parses a configuration file containing key/value pairs. The file must be in the UTF-8
3039 -     * encoding.
3040 -     *
3041 -     * @return a map from keys to values, where a value will be an array of strings if more than
3042 -     * one key/value pair in the config file was associated with the same key.
3043 +     * Parses the data for a config instance from the supplied {@code source} reader.
3044 +     * @return a map that can be used to create a {@link #Config}.
3045       */
3046 -    public static Config parseConfig (File source, ParseOpts opts)
3047 +    public static Map<String, Object> parseData (Reader source, ParseOpts opts)
3048          throws IOException
3049      {
3050          Map<String, Object> data = new HashMap<>();
3051 @@ -196,15 +191,34 @@ public class Config
3052              }
3053          }
3054  
3055 -        // special magic for the getdown.txt config: if the parsed data contains 'strict_comments =
3056 -        // true' then we reparse the file with strict comments (i.e. # is only assumed to start a
3057 -        // comment in column 0)
3058 -        if (!opts.strictComments && Boolean.parseBoolean((String)data.get("strict_comments"))) {
3059 -            opts.strictComments = true;
3060 -            return parseConfig(source, opts);
3061 -        }
3062 +        return data;
3063 +    }
3064 +
3065 +    /**
3066 +     * Parses a configuration file containing key/value pairs. The file must be in the UTF-8
3067 +     * encoding.
3068 +     *
3069 +     * @return a map from keys to values, where a value will be an array of strings if more than
3070 +     * one key/value pair in the config file was associated with the same key.
3071 +     */
3072 +    public static Config parseConfig (File source, ParseOpts opts)
3073 +        throws IOException
3074 +    {
3075 +        // annoyingly FileReader does not allow encoding to be specified (uses platform default)
3076 +        try (FileInputStream fis = new FileInputStream(source);
3077 +             InputStreamReader input = new InputStreamReader(fis, StandardCharsets.UTF_8)) {
3078 +            Map<String, Object> data = parseData(input, opts);
3079 +
3080 +            // special magic for the getdown.txt config: if the parsed data contains
3081 +            // 'strict_comments = true' then we reparse the file with strict comments (i.e. # is
3082 +            // only assumed to start a comment in column 0)
3083 +            if (!opts.strictComments && Boolean.parseBoolean((String)data.get("strict_comments"))) {
3084 +                opts.strictComments = true;
3085 +                return parseConfig(source, opts);
3086 +            }
3087  
3088 -        return new Config(data);
3089 +            return new Config(data);
3090 +        }
3091      }
3092  
3093      public Config (Map<String,  Object> data) {
3094 @@ -248,6 +262,20 @@ public class Config
3095          return Boolean.parseBoolean(getString(name));
3096      }
3097  
3098 +    /**
3099 +     * Returns the specified config value as an enum value. The string value of the config is
3100 +     * converted to all upper case and then turned into an enum via {@link Enum#valueOf}.
3101 +     */
3102 +    public <T extends Enum<T>> T getEnum (String name, Class<T> eclass, T defval) {
3103 +        String value = getString(name, defval.toString());
3104 +        try {
3105 +            return Enum.valueOf(eclass, value.toUpperCase());
3106 +        } catch (Exception e) {
3107 +            log.warning("Invalid value for '" + name + "' config: '" + value + "'.");
3108 +            return defval;
3109 +        }
3110 +    }
3111 +
3112      /**
3113       * Massages a single string into an array and leaves existing array values as is. Simplifies
3114       * access to parameters that are expected to be arrays.
3115 @@ -255,9 +283,6 @@ public class Config
3116      public String[] getMultiValue (String name)
3117      {
3118          Object value = _data.get(name);
3119 -        if (value == null) {
3120 -          return new String[] {};
3121 -        }
3122          if (value instanceof String) {
3123              return new String[] { (String)value };
3124          } else {
3125 @@ -355,7 +380,7 @@ public class Config
3126      protected static boolean checkQualifiers (String quals, String osname, String osarch)
3127      {
3128          if (quals.startsWith("!")) {
3129 -            if (quals.indexOf(",") != -1) { // sanity check
3130 +            if (quals.contains(",")) { // sanity check
3131                  log.warning("Multiple qualifiers cannot be used when one of the qualifiers " +
3132                              "is negative", "quals", quals);
3133                  return false;
3134 @@ -375,103 +400,8 @@ public class Config
3135      {
3136          String[] bits = qual.trim().toLowerCase(Locale.ROOT).split("-");
3137          String os = bits[0], arch = (bits.length > 1) ? bits[1] : "";
3138 -        return (osname.indexOf(os) != -1) && (osarch.indexOf(arch) != -1);
3139 -    }
3140 -    
3141 -    public void mergeConfig(Config newValues, boolean merge) {
3142 -      
3143 -      for (Map.Entry<String, Object> entry : newValues.getData().entrySet()) {
3144 -        
3145 -        String key = entry.getKey();
3146 -        Object nvalue = entry.getValue();
3147 -
3148 -        String mkey = key.indexOf('.') > -1 ? key.substring(key.indexOf('.') + 1) : key;
3149 -        if (merge && allowedMergeKeys.contains(mkey)) {
3150 -          
3151 -          // merge multi values
3152 -          
3153 -          Object value = _data.get(key);
3154 -          
3155 -          if (value == null) {
3156 -            _data.put(key, nvalue);
3157 -          } else if (value instanceof String) {
3158 -            if (nvalue instanceof String) {
3159 -              
3160 -              // value is String, nvalue is String
3161 -              _data.put(key, new String[] { (String)value, (String)nvalue });
3162 -              
3163 -            } else if (nvalue instanceof String[]) {
3164 -              
3165 -              // value is String, nvalue is String[]
3166 -              String[] nvalues = (String[])nvalue;
3167 -              String[] newvalues = new String[nvalues.length+1];
3168 -              newvalues[0] = (String)value;
3169 -              System.arraycopy(nvalues, 0, newvalues, 1, nvalues.length);
3170 -              _data.put(key, newvalues);
3171 -              
3172 -            }
3173 -          } else if (value instanceof String[]) {
3174 -            if (nvalue instanceof String) {
3175 -              
3176 -              // value is String[], nvalue is String
3177 -              String[] values = (String[])value;
3178 -              String[] newvalues = new String[values.length+1];
3179 -              System.arraycopy(values, 0, newvalues, 0, values.length);
3180 -              newvalues[values.length] = (String)nvalue;
3181 -              _data.put(key, newvalues);
3182 -              
3183 -            } else if (nvalue instanceof String[]) {
3184 -              
3185 -              // value is String[], nvalue is String[]
3186 -              String[] values = (String[])value;
3187 -              String[] nvalues = (String[])nvalue;
3188 -              String[] newvalues = new String[values.length + nvalues.length];
3189 -              System.arraycopy(values, 0, newvalues, 0, values.length);
3190 -              System.arraycopy(nvalues, 0, newvalues, values.length, newvalues.length);
3191 -              _data.put(key, newvalues);
3192 -              
3193 -            }
3194 -          }
3195 -          
3196 -        } else if (allowedReplaceKeys.contains(mkey)){
3197 -          
3198 -          // replace value
3199 -          _data.put(key, nvalue);
3200 -          
3201 -        } else {
3202 -          log.warning("Not merging key '"+key+"' into config");
3203 -        }
3204 -
3205 -      }
3206 -      
3207 -    }
3208 -    
3209 -    public String toString() {
3210 -      StringBuilder sb = new StringBuilder();
3211 -      for (Map.Entry<String, Object> entry : getData().entrySet()) {
3212 -        String key = entry.getKey();
3213 -        Object val = entry.getValue();
3214 -        sb.append(key);
3215 -        sb.append("=");
3216 -        if (val instanceof String) {
3217 -          sb.append((String)val);
3218 -        } else if (val instanceof String[]) {
3219 -          sb.append(Arrays.toString((String[])val));
3220 -        } else {
3221 -          sb.append("Value not String or String[]");
3222 -        }
3223 -        sb.append("\n");
3224 -      }
3225 -      return sb.toString();
3226 -    }
3227 -    
3228 -    public Map<String, Object> getData() {
3229 -      return _data;
3230 +        return (osname.contains(os)) && (osarch.contains(arch));
3231      }
3232  
3233      private final Map<String, Object> _data;
3234
3235 -    public static final List<String> allowedReplaceKeys = Arrays.asList("appbase","apparg","jvmarg"); // these are the ones we might use
3236 -    public static final List<String> allowedMergeKeys = Arrays.asList("apparg","jvmarg"); // these are the ones we might use
3237 -    //private final List<String> allowedMergeKeys = Arrays.asList("apparg","jvmarg","resource","code","java_location"); // (not exhaustive list here)
3238  }
3239 diff --git a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/util/ConnectionUtil.java b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/util/ConnectionUtil.java
3240 deleted file mode 100644
3241 index 21b056932..000000000
3242 --- a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/util/ConnectionUtil.java
3243 +++ /dev/null
3244 @@ -1,73 +0,0 @@
3245 -//
3246 -// Getdown - application installer, patcher and launcher
3247 -// Copyright (C) 2004-2018 Getdown authors
3248 -// https://github.com/threerings/getdown/blob/master/LICENSE
3249 -
3250 -package com.threerings.getdown.util;
3251 -
3252 -import java.io.IOException;
3253 -import java.net.HttpURLConnection;
3254 -import java.net.Proxy;
3255 -import java.net.URL;
3256 -import java.net.URLConnection;
3257 -import java.net.URLDecoder;
3258 -
3259 -import com.threerings.getdown.data.SysProps;
3260 -
3261 -import static java.nio.charset.StandardCharsets.UTF_8;
3262 -
3263 -public class ConnectionUtil
3264 -{
3265 -    /**
3266 -     * Opens a connection to a URL, setting the authentication header if user info is present.
3267 -     * @param proxy the proxy via which to perform HTTP connections.
3268 -     * @param url the URL to which to open a connection.
3269 -     * @param connectTimeout if {@code > 0} then a timeout, in seconds, to use when opening the
3270 -     * connection. If {@code 0} is supplied, the connection timeout specified via system properties
3271 -     * will be used instead.
3272 -     * @param readTimeout if {@code > 0} then a timeout, in seconds, to use while reading data from
3273 -     * the connection. If {@code 0} is supplied, the read timeout specified via system properties
3274 -     * will be used instead.
3275 -     */
3276 -    public static URLConnection open (Proxy proxy, URL url, int connectTimeout, int readTimeout)
3277 -        throws IOException
3278 -    {
3279 -        URLConnection conn = url.openConnection(proxy);
3280 -
3281 -        // configure a connect timeout, if requested
3282 -        int ctimeout = connectTimeout > 0 ? connectTimeout : SysProps.connectTimeout();
3283 -        if (ctimeout > 0) {
3284 -            conn.setConnectTimeout(ctimeout * 1000);
3285 -        }
3286 -
3287 -        // configure a read timeout, if requested
3288 -        int rtimeout = readTimeout > 0 ? readTimeout : SysProps.readTimeout();
3289 -        if (rtimeout > 0) {
3290 -            conn.setReadTimeout(rtimeout * 1000);
3291 -        }
3292 -
3293 -        // If URL has a username:password@ before hostname, use HTTP basic auth
3294 -        String userInfo = url.getUserInfo();
3295 -        if (userInfo != null) {
3296 -            // Remove any percent-encoding in the username/password
3297 -            userInfo = URLDecoder.decode(userInfo, "UTF-8");
3298 -            // Now base64 encode the auth info and make it a single line
3299 -            String encoded = Base64.encodeToString(userInfo.getBytes(UTF_8), Base64.DEFAULT).
3300 -                replaceAll("\\n","").replaceAll("\\r", "");
3301 -            conn.setRequestProperty("Authorization", "Basic " + encoded);
3302 -        }
3303 -
3304 -        return conn;
3305 -    }
3306 -
3307 -    /**
3308 -     * Opens a connection to a http or https URL, setting the authentication header if user info is
3309 -     * present. Throws a class cast exception if the connection returned is not the right type. See
3310 -     * {@link #open} for parameter documentation.
3311 -     */
3312 -    public static HttpURLConnection openHttp (
3313 -        Proxy proxy, URL url, int connectTimeout, int readTimeout) throws IOException
3314 -    {
3315 -        return (HttpURLConnection)open(proxy, url, connectTimeout, readTimeout);
3316 -    }
3317 -}
3318 diff --git a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/util/FileUtil.java b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/util/FileUtil.java
3319 index 67d033086..d7de78bbd 100644
3320 --- a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/util/FileUtil.java
3321 +++ b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/util/FileUtil.java
3322 @@ -6,25 +6,17 @@
3323  package com.threerings.getdown.util;
3324  
3325  import java.io.*;
3326 -import java.nio.file.Files;
3327 -import java.nio.file.Paths;
3328  import java.util.*;
3329  import java.util.jar.*;
3330 -import java.util.zip.GZIPInputStream;
3331 +import java.util.zip.*;
3332  
3333 -import org.apache.commons.compress.archivers.ArchiveEntry;
3334 -import org.apache.commons.compress.archivers.ArchiveInputStream;
3335 -import org.apache.commons.compress.archivers.tar.TarArchiveEntry;
3336 -import org.apache.commons.compress.archivers.tar.TarArchiveInputStream;
3337 -
3338 -import com.threerings.getdown.util.StreamUtil;
3339  import com.threerings.getdown.Log;
3340  import static com.threerings.getdown.Log.log;
3341  
3342  /**
3343   * File related utilities.
3344   */
3345 -public class FileUtil
3346 +public final class FileUtil
3347  {
3348      /**
3349       * Gets the specified source file to the specified destination file by hook or crook. Windows
3350 @@ -122,13 +114,13 @@ public class FileUtil
3351       * @param cleanExistingDirs if true, all files in all directories contained in {@code jar} will
3352       * be deleted prior to unpacking the jar.
3353       */
3354 -    public static void unpackJar (JarFile jar, File target, boolean cleanExistingDirs)
3355 +    public static void unpackJar (ZipFile jar, File target, boolean cleanExistingDirs)
3356          throws IOException
3357      {
3358          if (cleanExistingDirs) {
3359 -            Enumeration<?> entries = jar.entries();
3360 +            Enumeration<? extends ZipEntry> entries = jar.entries();
3361              while (entries.hasMoreElements()) {
3362 -                JarEntry entry = (JarEntry)entries.nextElement();
3363 +                ZipEntry entry = entries.nextElement();
3364                  if (entry.isDirectory()) {
3365                      File efile = new File(target, entry.getName());
3366                      if (efile.exists()) {
3367 @@ -141,9 +133,9 @@ public class FileUtil
3368              }
3369          }
3370  
3371 -        Enumeration<?> entries = jar.entries();
3372 +        Enumeration<? extends ZipEntry> entries = jar.entries();
3373          while (entries.hasMoreElements()) {
3374 -            JarEntry entry = (JarEntry)entries.nextElement();
3375 +            ZipEntry entry = entries.nextElement();
3376              File efile = new File(target, entry.getName());
3377  
3378              // if we're unpacking a normal jar file, it will have special path
3379 @@ -164,7 +156,7 @@ public class FileUtil
3380              }
3381  
3382              try (BufferedOutputStream fout = new BufferedOutputStream(new FileOutputStream(efile));
3383 -                InputStream jin = jar.getInputStream(entry)) {
3384 +                 InputStream jin = jar.getInputStream(entry)) {
3385                  StreamUtil.copy(jin, fout);
3386              } catch (Exception e) {
3387                  throw new IOException(
3388 @@ -174,82 +166,13 @@ public class FileUtil
3389      }
3390  
3391      /**
3392 -     * Unpacks the specified tgz file into the specified target directory.
3393 -     * @param cleanExistingDirs if true, all files in all directories contained in {@code tgz} will
3394 -     * be deleted prior to unpacking the tgz.
3395 -     */
3396 -    public static void unpackTgz (TarArchiveInputStream tgz, File target, boolean cleanExistingDirs)
3397 -        throws IOException
3398 -    {
3399 -               TarArchiveEntry entry;
3400 -               while ((entry = tgz.getNextTarEntry()) != null) {
3401 -            // sanitize the entry name
3402 -                       String entryName = entry.getName();
3403 -                       if (entryName.startsWith(File.separator))
3404 -                       {
3405 -                               entryName = entryName.substring(File.separator.length());
3406 -                       }
3407 -            File efile = new File(target, entryName);
3408 -
3409 -            // if we're unpacking a normal tgz file, it will have special path
3410 -            // entries that allow us to create our directories first
3411 -            if (entry.isDirectory()) {
3412 -               
3413 -                               if (cleanExistingDirs) {
3414 -                    if (efile.exists()) {
3415 -                        for (File f : efile.listFiles()) {
3416 -                            if (!f.isDirectory())
3417 -                            f.delete();
3418 -                        }
3419 -                    }
3420 -                               }
3421 -                               
3422 -                if (!efile.exists() && !efile.mkdir()) {
3423 -                    log.warning("Failed to create tgz entry path", "tgz", tgz, "entry", entry);
3424 -                }
3425 -                continue;
3426 -            }
3427 -
3428 -            // but some do not, so we want to ensure that our directories exist
3429 -            // prior to getting down and funky
3430 -            File parent = new File(efile.getParent());
3431 -            if (!parent.exists() && !parent.mkdirs()) {
3432 -                log.warning("Failed to create tgz entry parent", "tgz", tgz, "parent", parent);
3433 -                continue;
3434 -            }
3435 -
3436 -            if (entry.isLink())
3437 -            {
3438 -               System.out.println("Creating hard link "+efile.getName()+" -> "+entry.getLinkName());
3439 -               Files.createLink(efile.toPath(), Paths.get(entry.getLinkName()));
3440 -               continue;
3441 -            }
3442 -
3443 -            if (entry.isSymbolicLink())
3444 -            {
3445 -               System.out.println("Creating symbolic link "+efile.getName()+" -> "+entry.getLinkName());
3446 -               Files.createSymbolicLink(efile.toPath(), Paths.get(entry.getLinkName()));
3447 -               continue;
3448 -            }
3449 -            
3450 -            try (BufferedOutputStream fout = new BufferedOutputStream(new FileOutputStream(efile));
3451 -                InputStream tin = tgz;) {
3452 -                StreamUtil.copy(tin, fout);
3453 -            } catch (Exception e) {
3454 -                throw new IOException(
3455 -                    Log.format("Failure unpacking", "tgz", tgz, "entry", efile), e);
3456 -            }
3457 -        }
3458 -    }
3459 -
3460 -    /**
3461 -     * Unpacks a pack200 packed jar file from {@code packedJar} into {@code target}. If {@code
3462 -     * packedJar} has a {@code .gz} extension, it will be gunzipped first.
3463 +     * Unpacks a pack200 packed jar file from {@code packedJar} into {@code target}.
3464 +     * If {@code packedJar} has a {@code .gz} extension, it will be gunzipped first.
3465       */
3466      public static void unpackPacked200Jar (File packedJar, File target) throws IOException
3467      {
3468          try (InputStream packJarIn = new FileInputStream(packedJar);
3469 -             JarOutputStream jarOut = new JarOutputStream(new FileOutputStream(target))) {
3470 +            JarOutputStream jarOut = new JarOutputStream(new FileOutputStream(target))) {
3471              boolean gz = (packedJar.getName().endsWith(".gz") ||
3472                            packedJar.getName().endsWith(".gz_new"));
3473              try (InputStream packJarIn2 = (gz ? new GZIPInputStream(packJarIn) : packJarIn)) {
3474 diff --git a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/util/HostWhitelist.java b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/util/HostWhitelist.java
3475 index f2f7ef39f..c05992abd 100644
3476 --- a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/util/HostWhitelist.java
3477 +++ b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/util/HostWhitelist.java
3478 @@ -25,8 +25,6 @@ public final class HostWhitelist
3479       */
3480      public static URL verify (URL url) throws MalformedURLException
3481      {
3482 -        
3483 -      
3484          return verify(Build.hostWhitelist(), url);
3485      }
3486  
3487 @@ -43,12 +41,6 @@ public final class HostWhitelist
3488          }
3489  
3490          String urlHost = url.getHost();
3491 -        String protocol = url.getProtocol();
3492 -        
3493 -        if (ALLOW_LOCATOR_FILE_PROTOCOL && protocol.equals("file") && urlHost.equals("")) {
3494 -          return url;
3495 -        }
3496 -        
3497          for (String host : hosts) {
3498              String regex = host.replace(".", "\\.").replace("*", ".*");
3499              if (urlHost.matches(regex)) {
3500 @@ -59,5 +51,4 @@ public final class HostWhitelist
3501          throw new MalformedURLException(
3502              "The host for the specified URL (" + url + ") is not in the host whitelist: " + hosts);
3503      }
3504 -    private static boolean ALLOW_LOCATOR_FILE_PROTOCOL = true;
3505  }
3506 diff --git a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/util/LaunchUtil.java b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/util/LaunchUtil.java
3507 index f2cd5736e..cc51794ef 100644
3508 --- a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/util/LaunchUtil.java
3509 +++ b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/util/LaunchUtil.java
3510 @@ -17,21 +17,23 @@ import static com.threerings.getdown.Log.log;
3511   * Useful routines for launching Java applications from within other Java
3512   * applications.
3513   */
3514 -public class LaunchUtil
3515 +public final class LaunchUtil
3516  {
3517 -    /** The directory into which a local VM installation should be unpacked. */
3518 -    public static final String LOCAL_JAVA_DIR = "jre";
3519 +    /** The default directory into which a local VM installation should be unpacked. */
3520 +    public static final String LOCAL_JAVA_DIR = "java_vm";
3521  
3522      /**
3523 -     * Writes a <code>version.txt</code> file into the specified application directory and
3524 +     * Writes a {@code version.txt} file into the specified application directory and
3525       * attempts to relaunch Getdown in that directory which will cause it to upgrade to the newly
3526       * specified version and relaunch the application.
3527       *
3528       * @param appdir the directory in which the application is installed.
3529       * @param getdownJarName the name of the getdown jar file in the application directory. This is
3530 -     * probably <code>getdown-pro.jar</code> or <code>getdown-retro-pro.jar</code> if you are using
3531 +     * probably {@code getdown-pro.jar} or {@code getdown-retro-pro.jar} if you are using
3532       * the results of the standard build.
3533       * @param newVersion the new version to which Getdown will update when it is executed.
3534 +     * @param javaLocalDir the name of the directory (inside {@code appdir}) that contains a
3535 +     * locally installed JRE. Defaults to {@link #LOCAL_JAVA_DIR} if null is passed.
3536       *
3537       * @return true if the relaunch succeeded, false if we were unable to relaunch due to being on
3538       * Windows 9x where we cannot launch subprocesses without waiting around for them to exit,
3539 @@ -39,13 +41,13 @@ public class LaunchUtil
3540       * after making this call as it will be upgraded and restarted. If false is returned, the
3541       * application should tell the user that they must restart the application manually.
3542       *
3543 -     * @exception IOException thrown if we were unable to create the <code>version.txt</code> file
3544 +     * @exception IOException thrown if we were unable to create the {@code version.txt} file
3545       * in the supplied application directory. If the version.txt file cannot be created, restarting
3546       * Getdown will not cause the application to be upgraded, so the application will have to
3547       * resort to telling the user that it is in a bad way.
3548       */
3549      public static boolean updateVersionAndRelaunch (
3550 -            File appdir, String getdownJarName, String newVersion)
3551 +            File appdir, String getdownJarName, String newVersion, String javaLocalDir)
3552          throws IOException
3553      {
3554          // create the file that instructs Getdown to upgrade
3555 @@ -61,9 +63,9 @@ public class LaunchUtil
3556          }
3557  
3558          // do the deed
3559 -        String[] args = new String[] {
3560 -            getJVMPath(appdir), "-jar", pro.toString(), appdir.getPath()
3561 -        };
3562 +        String javaDir = StringUtil.isBlank(javaLocalDir) ? LOCAL_JAVA_DIR : javaLocalDir;
3563 +        String javaBin = getJVMBinaryPath(new File(appdir, javaDir), false);
3564 +        String[] args = { javaBin, "-jar", pro.toString(), appdir.getPath() };
3565          log.info("Running " + StringUtil.join(args, "\n  "));
3566          try {
3567              Runtime.getRuntime().exec(args, null);
3568 @@ -75,25 +77,15 @@ public class LaunchUtil
3569      }
3570  
3571      /**
3572 -     * Reconstructs the path to the JVM used to launch this process.
3573 -     */
3574 -    public static String getJVMPath (File appdir)
3575 -    {
3576 -        return getJVMPath(appdir, false);
3577 -    }
3578 -
3579 -    /**
3580 -     * Reconstructs the path to the JVM used to launch this process.
3581 -     *
3582 +     * Resolves a path to a JVM binary.
3583 +     * @param javaLocalDir JRE location within appdir.
3584       * @param windebug if true we will use java.exe instead of javaw.exe on Windows.
3585 +     * @return the path to the JVM binary used to launch this process.
3586       */
3587 -    public static String getJVMPath (File appdir, boolean windebug)
3588 +    public static String getJVMBinaryPath (File javaLocalDir, boolean windebug)
3589      {
3590          // first look in our application directory for an installed VM
3591 -        String vmpath = checkJVMPath(new File(appdir, LOCAL_JAVA_DIR).getAbsolutePath(), windebug);
3592 -        if (vmpath == null && isMacOS()) {
3593 -                       vmpath = checkJVMPath(new File(appdir, LOCAL_JAVA_DIR + "/Contents/Home").getAbsolutePath(), windebug);
3594 -        }
3595 +        String vmpath = checkJVMPath(javaLocalDir.getAbsolutePath(), windebug);
3596  
3597          // then fall back to the VM in which we're already running
3598          if (vmpath == null) {
3599 @@ -102,7 +94,7 @@ public class LaunchUtil
3600  
3601          // then throw up our hands and hope for the best
3602          if (vmpath == null) {
3603 -            log.warning("Unable to find java [appdir=" + appdir +
3604 +            log.warning("Unable to find java [local=" + javaLocalDir +
3605                          ", java.home=" + System.getProperty("java.home") + "]!");
3606              vmpath = "java";
3607          }
3608 @@ -153,7 +145,7 @@ public class LaunchUtil
3609              if (newgd.renameTo(curgd)) {
3610                  FileUtil.deleteHarder(oldgd); // yay!
3611                  try {
3612 -                    // copy the moved file back to getdown-dop-new.jar so that we don't end up
3613 +                    // copy the moved file back to newgd so that we don't end up
3614                      // downloading another copy next time
3615                      FileUtil.copy(curgd, newgd);
3616                  } catch (IOException e) {
3617 @@ -185,23 +177,23 @@ public class LaunchUtil
3618      public static boolean mustMonitorChildren ()
3619      {
3620          String osname = System.getProperty("os.name", "").toLowerCase(Locale.ROOT);
3621 -        return (osname.indexOf("windows 98") != -1 || osname.indexOf("windows me") != -1);
3622 +        return (osname.contains("windows 98") || osname.contains("windows me"));
3623      }
3624  
3625      /**
3626       * Returns true if we're running in a JVM that identifies its operating system as Windows.
3627       */
3628 -    public static final boolean isWindows () { return _isWindows; }
3629 +    public static boolean isWindows () { return _isWindows; }
3630  
3631      /**
3632       * Returns true if we're running in a JVM that identifies its operating system as MacOS.
3633       */
3634 -    public static final boolean isMacOS () { return _isMacOS; }
3635 +    public static boolean isMacOS () { return _isMacOS; }
3636  
3637      /**
3638       * Returns true if we're running in a JVM that identifies its operating system as Linux.
3639       */
3640 -    public static final boolean isLinux () { return _isLinux; }
3641 +    public static boolean isLinux () { return _isLinux; }
3642  
3643      /**
3644       * Checks whether a Java Virtual Machine can be located in the supplied path.
3645 @@ -240,10 +232,9 @@ public class LaunchUtil
3646          try {
3647              String osname = System.getProperty("os.name");
3648              osname = (osname == null) ? "" : osname;
3649 -            _isWindows = (osname.indexOf("Windows") != -1);
3650 -            _isMacOS = (osname.indexOf("Mac OS") != -1 ||
3651 -                        osname.indexOf("MacOS") != -1);
3652 -            _isLinux = (osname.indexOf("Linux") != -1);
3653 +            _isWindows = (osname.contains("Windows"));
3654 +            _isMacOS = (osname.contains("Mac OS") || osname.contains("MacOS"));
3655 +            _isLinux = (osname.contains("Linux"));
3656          } catch (Exception e) {
3657              // can't grab system properties; we'll just pretend we're not on any of these OSes
3658          }
3659 diff --git a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/util/MessageUtil.java b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/util/MessageUtil.java
3660 index 28dbdcff5..f2ee9a265 100644
3661 --- a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/util/MessageUtil.java
3662 +++ b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/util/MessageUtil.java
3663 @@ -5,7 +5,7 @@
3664  
3665  package com.threerings.getdown.util;
3666  
3667 -public class MessageUtil {
3668 +public final class MessageUtil {
3669  
3670      /**
3671       * Returns whether or not the provided string is tainted. See {@link #taint}. Null strings
3672 @@ -101,11 +101,11 @@ public class MessageUtil {
3673      }
3674  
3675      /**
3676 -     * Used to escape single quotes so that they are not interpreted by <code>MessageFormat</code>.
3677 +     * Used to escape single quotes so that they are not interpreted by {@code MessageFormat}.
3678       * As we assume all single quotes are to be escaped, we cannot use the characters
3679       * <code>{</code> and <code>}</code> in our translation strings, but this is a small price to
3680       * pay to have to differentiate between messages that will and won't eventually be parsed by a
3681 -     * <code>MessageFormat</code> instance.
3682 +     * {@code MessageFormat} instance.
3683       */
3684      public static String escape (String message)
3685      {
3686 diff --git a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/util/ProgressObserver.java b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/util/ProgressObserver.java
3687 index ad4c560a4..ac3e0a3de 100644
3688 --- a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/util/ProgressObserver.java
3689 +++ b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/util/ProgressObserver.java
3690 @@ -14,5 +14,5 @@ public interface ProgressObserver
3691       * Informs the observer that we have completed the specified
3692       * percentage of the process.
3693       */
3694 -    public void progress (int percent);
3695 +    void progress (int percent);
3696  }
3697 diff --git a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/util/StreamUtil.java b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/util/StreamUtil.java
3698 index 373cfff00..5cb1a405b 100644
3699 --- a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/util/StreamUtil.java
3700 +++ b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/util/StreamUtil.java
3701 @@ -11,11 +11,10 @@ import java.io.InputStream;
3702  import java.io.OutputStream;
3703  import java.io.Reader;
3704  import java.io.Writer;
3705 -import java.nio.charset.Charset;
3706  
3707  import static com.threerings.getdown.Log.log;
3708  
3709 -public class StreamUtil {
3710 +public final class StreamUtil {
3711      /**
3712       * Convenient close for a stream. Use in a finally clause and love life.
3713       */
3714 diff --git a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/util/StringUtil.java b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/util/StringUtil.java
3715 index 03d3c9ccd..86277c881 100644
3716 --- a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/util/StringUtil.java
3717 +++ b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/util/StringUtil.java
3718 @@ -7,14 +7,14 @@ package com.threerings.getdown.util;
3719  
3720  import java.util.StringTokenizer;
3721  
3722 -public class StringUtil {
3723 +public final class StringUtil {
3724  
3725      /**
3726       * @return true if the specified string could be a valid URL (contains no illegal characters)
3727       */
3728      public static boolean couldBeValidUrl (String url)
3729      {
3730 -        return url.matches("[A-Za-z0-9\\-\\._~:/\\?#\\[\\]@!$&'\\(\\)\\*\\+,;=%]+");
3731 +        return url.matches("[A-Za-z0-9\\-._~:/?#\\[\\]@!$&'()*+,;=%]+");
3732      }
3733  
3734      /**
3735 @@ -84,7 +84,7 @@ public class StringUtil {
3736          source = source.replace(",,", "%COMMA%");
3737  
3738          // count up the number of tokens
3739 -        while ((tpos = source.indexOf(",", tpos+1)) != -1) {
3740 +        while ((tpos = source.indexOf(',', tpos+1)) != -1) {
3741              tcount++;
3742          }
3743  
3744 @@ -92,7 +92,7 @@ public class StringUtil {
3745          tpos = -1; tcount = 0;
3746  
3747          // do the split
3748 -        while ((tpos = source.indexOf(",", tpos+1)) != -1) {
3749 +        while ((tpos = source.indexOf(',', tpos+1)) != -1) {
3750              tokens[tcount] = source.substring(tstart, tpos);
3751              tokens[tcount] = tokens[tcount].trim().replace("%COMMA%", ",");
3752              if (intern) {
3753 @@ -119,7 +119,7 @@ public class StringUtil {
3754  
3755      /**
3756       * Generates a string from the supplied bytes that is the HEX encoded representation of those
3757 -     * bytes.  Returns the empty string for a <code>null</code> or empty byte array.
3758 +     * bytes.  Returns the empty string for a {@code null} or empty byte array.
3759       *
3760       * @param bytes the bytes for which we want a string representation.
3761       * @param count the number of bytes to stop at (which will be coerced into being {@code <=} the
3762 @@ -185,7 +185,7 @@ public class StringUtil {
3763      }
3764  
3765      /**
3766 -     * Helper function for the various <code>join</code> methods.
3767 +     * Helper function for the various {@code join} methods.
3768       */
3769      protected static String join (Object[] values, String separator, boolean escape)
3770      {
3771 @@ -201,6 +201,6 @@ public class StringUtil {
3772          return buf.toString();
3773      }
3774  
3775 -    /** Used by {@link #hexlate} and {@link #unhexlate}. */
3776 +    /** Used by {@link #hexlate}. */
3777      protected static final String XLATE = "0123456789abcdef";
3778  }
3779 diff --git a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/util/VersionUtil.java b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/util/VersionUtil.java
3780 index 49e4e6e0e..b2f289415 100644
3781 --- a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/util/VersionUtil.java
3782 +++ b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/util/VersionUtil.java
3783 @@ -22,7 +22,7 @@ import static java.nio.charset.StandardCharsets.UTF_8;
3784  /**
3785   * Version related utilities.
3786   */
3787 -public class VersionUtil
3788 +public final class VersionUtil
3789  {
3790      /**
3791       * Reads a version number from a file.
3792 diff --git a/getdown/src/getdown/core/src/main/java/jalview/bin/MemorySetting.java b/getdown/src/getdown/core/src/main/java/jalview/bin/MemorySetting.java
3793 deleted file mode 100644
3794 index 8af09da6d..000000000
3795 --- a/getdown/src/getdown/core/src/main/java/jalview/bin/MemorySetting.java
3796 +++ /dev/null
3797 @@ -1,51 +0,0 @@
3798 -package jalview.bin;
3799 -
3800 -import java.lang.management.ManagementFactory;
3801 -import java.lang.management.OperatingSystemMXBean;
3802 -
3803 -public class MemorySetting
3804 -{
3805 -  public static final long leaveFreeMinMemory = 536870912; // 0.5 GB
3806 -
3807 -  public static final long applicationMinMemory = 536870912; // 0.5 GB
3808 -
3809 -  protected static long getPhysicalMemory()
3810 -  {
3811 -    final OperatingSystemMXBean o = ManagementFactory
3812 -            .getOperatingSystemMXBean();
3813 -
3814 -    try
3815 -    {
3816 -      if (o instanceof com.sun.management.OperatingSystemMXBean)
3817 -      {
3818 -        final com.sun.management.OperatingSystemMXBean osb = (com.sun.management.OperatingSystemMXBean) o;
3819 -        return osb.getTotalPhysicalMemorySize();
3820 -      }
3821 -    } catch (NoClassDefFoundError e)
3822 -    {
3823 -      // com.sun.management.OperatingSystemMXBean doesn't exist in this JVM
3824 -      System.err.println("No com.sun.management.OperatingSystemMXBean");
3825 -    }
3826 -
3827 -    // We didn't get a com.sun.management.OperatingSystemMXBean.
3828 -    return -1;
3829 -  }
3830 -
3831 -  public static long memPercent(int percent)
3832 -  {
3833 -    long memPercent = -1;
3834 -
3835 -    long physicalMem = getPhysicalMemory();
3836 -    if (physicalMem > applicationMinMemory)
3837 -    {
3838 -      // try and set at least applicationMinMemory and thereafter ensure
3839 -      // leaveFreeMinMemory is left for the OS
3840 -      memPercent = Math.max(applicationMinMemory,
3841 -              physicalMem - Math.max(physicalMem * (100 - percent) / 100,
3842 -                      leaveFreeMinMemory));
3843 -    }
3844 -
3845 -    return memPercent;
3846 -  }
3847 -
3848 -}
3849 diff --git a/getdown/src/getdown/core/src/test/java/com/threerings/getdown/cache/GarbageCollectorTest.java b/getdown/src/getdown/core/src/test/java/com/threerings/getdown/cache/GarbageCollectorTest.java
3850 index d5a393745..c7fcf7271 100644
3851 --- a/getdown/src/getdown/core/src/test/java/com/threerings/getdown/cache/GarbageCollectorTest.java
3852 +++ b/getdown/src/getdown/core/src/test/java/com/threerings/getdown/cache/GarbageCollectorTest.java
3853 @@ -7,23 +7,36 @@ package com.threerings.getdown.cache;
3854  
3855  import java.io.File;
3856  import java.io.IOException;
3857 +import java.util.Arrays;
3858 +import java.util.Collection;
3859  import java.util.concurrent.TimeUnit;
3860  
3861  import org.junit.*;
3862  import org.junit.rules.TemporaryFolder;
3863 -
3864 +import org.junit.runner.RunWith;
3865 +import org.junit.runners.Parameterized;
3866  import static org.junit.Assert.*;
3867  import static org.junit.Assume.assumeTrue;
3868  
3869  /**
3870   * Validates that cache garbage is collected and deleted correctly.
3871   */
3872 +@RunWith(Parameterized.class)
3873  public class GarbageCollectorTest
3874  {
3875 +    @Parameterized.Parameters(name = "{0}")
3876 +    public static Collection<Object[]> data() {
3877 +        return Arrays.asList(new Object[][] {{ ".jar" }, { ".zip" }});
3878 +    }
3879 +
3880 +    @Parameterized.Parameter
3881 +    public String extension;
3882 +
3883      @Before public void setupFiles () throws IOException
3884      {
3885 -        _cachedFile = _folder.newFile("abc123.jar");
3886 -        _lastAccessedFile = _folder.newFile("abc123.jar" + ResourceCache.LAST_ACCESSED_FILE_SUFFIX);
3887 +        _cachedFile = _folder.newFile("abc123" + extension);
3888 +        _lastAccessedFile = _folder.newFile(
3889 +            "abc123" + extension + ResourceCache.LAST_ACCESSED_FILE_SUFFIX);
3890      }
3891  
3892      @Test public void shouldDeleteCacheEntryIfRetentionPeriodIsReached ()
3893 diff --git a/getdown/src/getdown/core/src/test/java/com/threerings/getdown/cache/ResourceCacheTest.java b/getdown/src/getdown/core/src/test/java/com/threerings/getdown/cache/ResourceCacheTest.java
3894 index 860c72a37..6acffda32 100644
3895 --- a/getdown/src/getdown/core/src/test/java/com/threerings/getdown/cache/ResourceCacheTest.java
3896 +++ b/getdown/src/getdown/core/src/test/java/com/threerings/getdown/cache/ResourceCacheTest.java
3897 @@ -7,26 +7,38 @@ package com.threerings.getdown.cache;
3898  
3899  import java.io.File;
3900  import java.io.IOException;
3901 +import java.util.Arrays;
3902 +import java.util.Collection;
3903  import java.util.concurrent.TimeUnit;
3904  
3905  import org.junit.*;
3906  import org.junit.rules.TemporaryFolder;
3907 -
3908 +import org.junit.runner.RunWith;
3909 +import org.junit.runners.Parameterized;
3910  import static org.junit.Assert.*;
3911  
3912  /**
3913   * Asserts the correct functionality of the {@link ResourceCache}.
3914   */
3915 +@RunWith(Parameterized.class)
3916  public class ResourceCacheTest
3917  {
3918 +    @Parameterized.Parameters(name = "{0}")
3919 +    public static Collection<Object[]> data() {
3920 +        return Arrays.asList(new Object[][] {{ ".jar" }, { ".zip" }});
3921 +    }
3922 +
3923 +    @Parameterized.Parameter
3924 +    public String extension;
3925 +
3926      @Before public void setupCache () throws IOException {
3927 -        _fileToCache = _folder.newFile("filetocache.jar");
3928 +        _fileToCache = _folder.newFile("filetocache" + extension);
3929          _cache = new ResourceCache(_folder.newFolder(".cache"));
3930      }
3931  
3932      @Test public void shouldCacheFile () throws IOException
3933      {
3934 -        assertEquals("abc123.jar", cacheFile().getName());
3935 +        assertEquals("abc123" + extension, cacheFile().getName());
3936      }
3937  
3938      private File cacheFile() throws IOException
3939 @@ -36,7 +48,7 @@ public class ResourceCacheTest
3940  
3941      @Test public void shouldTrackFileUsage () throws IOException
3942      {
3943 -        String name = "abc123.jar" + ResourceCache.LAST_ACCESSED_FILE_SUFFIX;
3944 +        String name = "abc123" + extension + ResourceCache.LAST_ACCESSED_FILE_SUFFIX;
3945          File lastAccessedFile = new File(cacheFile().getParentFile(), name);
3946          assertTrue(lastAccessedFile.exists());
3947      }
3948 diff --git a/getdown/src/getdown/core/src/test/java/com/threerings/getdown/data/EnvConfigTest.java b/getdown/src/getdown/core/src/test/java/com/threerings/getdown/data/EnvConfigTest.java
3949 index 61786518c..04a73d38b 100644
3950 --- a/getdown/src/getdown/core/src/test/java/com/threerings/getdown/data/EnvConfigTest.java
3951 +++ b/getdown/src/getdown/core/src/test/java/com/threerings/getdown/data/EnvConfigTest.java
3952 @@ -19,13 +19,13 @@ public class EnvConfigTest {
3953      static String TESTID = "testid";
3954      static String TESTBASE = "https://test.com/test";
3955  
3956 -    private void debugNotes(List<EnvConfig.Note> notes) {
3957 +    private void debugNotes (List<EnvConfig.Note> notes) {
3958          for (EnvConfig.Note note : notes) {
3959              System.out.println(note.message);
3960          }
3961      }
3962  
3963 -    private void checkNoNotes (List<EnvConfig.Note> notes) {
3964 +    static void checkNoNotes (List<EnvConfig.Note> notes) {
3965          StringBuilder msg = new StringBuilder();
3966          for (EnvConfig.Note note : notes) {
3967              if (note.level != EnvConfig.Note.Level.INFO) {
3968 diff --git a/getdown/src/getdown/launcher/.project-MOVED b/getdown/src/getdown/launcher/.project-MOVED
3969 deleted file mode 100644
3970 index d77a6e8db..000000000
3971 --- a/getdown/src/getdown/launcher/.project-MOVED
3972 +++ /dev/null
3973 @@ -1,23 +0,0 @@
3974 -<?xml version="1.0" encoding="UTF-8"?>
3975 -<projectDescription>
3976 -       <name>getdown-launcher</name>
3977 -       <comment></comment>
3978 -       <projects>
3979 -       </projects>
3980 -       <buildSpec>
3981 -               <buildCommand>
3982 -                       <name>org.eclipse.jdt.core.javabuilder</name>
3983 -                       <arguments>
3984 -                       </arguments>
3985 -               </buildCommand>
3986 -               <buildCommand>
3987 -                       <name>org.eclipse.m2e.core.maven2Builder</name>
3988 -                       <arguments>
3989 -                       </arguments>
3990 -               </buildCommand>
3991 -       </buildSpec>
3992 -       <natures>
3993 -               <nature>org.eclipse.jdt.core.javanature</nature>
3994 -               <nature>org.eclipse.m2e.core.maven2Nature</nature>
3995 -       </natures>
3996 -</projectDescription>
3997 diff --git a/getdown/src/getdown/launcher/.settings/org.eclipse.core.resources.prefs b/getdown/src/getdown/launcher/.settings/org.eclipse.core.resources.prefs
3998 deleted file mode 100644
3999 index abdea9ac0..000000000
4000 --- a/getdown/src/getdown/launcher/.settings/org.eclipse.core.resources.prefs
4001 +++ /dev/null
4002 @@ -1,4 +0,0 @@
4003 -eclipse.preferences.version=1
4004 -encoding//src/main/java=UTF-8
4005 -encoding//src/main/resources=UTF-8
4006 -encoding/<project>=UTF-8
4007 diff --git a/getdown/src/getdown/launcher/.settings/org.eclipse.jdt.core.prefs b/getdown/src/getdown/launcher/.settings/org.eclipse.jdt.core.prefs
4008 deleted file mode 100644
4009 index 54e56721d..000000000
4010 --- a/getdown/src/getdown/launcher/.settings/org.eclipse.jdt.core.prefs
4011 +++ /dev/null
4012 @@ -1,6 +0,0 @@
4013 -eclipse.preferences.version=1
4014 -org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.7
4015 -org.eclipse.jdt.core.compiler.compliance=1.7
4016 -org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning
4017 -org.eclipse.jdt.core.compiler.release=disabled
4018 -org.eclipse.jdt.core.compiler.source=1.7
4019 diff --git a/getdown/src/getdown/launcher/.settings/org.eclipse.m2e.core.prefs b/getdown/src/getdown/launcher/.settings/org.eclipse.m2e.core.prefs
4020 deleted file mode 100644
4021 index f897a7f1c..000000000
4022 --- a/getdown/src/getdown/launcher/.settings/org.eclipse.m2e.core.prefs
4023 +++ /dev/null
4024 @@ -1,4 +0,0 @@
4025 -activeProfiles=
4026 -eclipse.preferences.version=1
4027 -resolveWorkspaceProjects=true
4028 -version=1
4029 diff --git a/getdown/src/getdown/launcher/pom.xml b/getdown/src/getdown/launcher/pom.xml
4030 index e77061a3b..f55234977 100644
4031 --- a/getdown/src/getdown/launcher/pom.xml
4032 +++ b/getdown/src/getdown/launcher/pom.xml
4033 @@ -4,7 +4,7 @@
4034    <parent>
4035      <groupId>com.threerings.getdown</groupId>
4036      <artifactId>getdown</artifactId>
4037 -    <version>1.8.3-SNAPSHOT</version>
4038 +    <version>1.8.7-SNAPSHOT</version>
4039    </parent>
4040  
4041    <artifactId>getdown-launcher</artifactId>
4042 @@ -24,11 +24,13 @@
4043        <groupId>com.threerings.getdown</groupId>
4044        <artifactId>getdown-core</artifactId>
4045        <version>${project.version}</version>
4046 +      <optional>true</optional>
4047      </dependency>
4048      <dependency>
4049        <groupId>com.samskivert</groupId>
4050        <artifactId>samskivert</artifactId>
4051        <version>1.2</version>
4052 +      <optional>true</optional>
4053      </dependency>
4054      <dependency>
4055        <groupId>jregistrykey</groupId>
4056 @@ -36,22 +38,47 @@
4057        <version>1.0</version>
4058        <optional>true</optional>
4059      </dependency>
4060 +    <dependency>
4061 +      <groupId>junit</groupId>
4062 +      <artifactId>junit</artifactId>
4063 +      <version>4.12</version>
4064 +      <scope>test</scope>
4065 +    </dependency>
4066    </dependencies>
4067  
4068    <build>
4069      <plugins>
4070 -      <!--
4071 +      <plugin>
4072 +        <groupId>org.codehaus.mojo</groupId>
4073 +        <artifactId>native2ascii-maven-plugin</artifactId>
4074 +        <version>2.0.1</version>
4075 +        <executions>
4076 +          <execution>
4077 +            <id>utf8-to-latin1</id>
4078 +            <goals>
4079 +              <goal>inplace</goal>
4080 +            </goals>
4081 +            <phase>process-resources</phase>
4082 +            <configuration>
4083 +              <dir>${project.build.outputDirectory}</dir>
4084 +              <encoding>${project.build.sourceEncoding}</encoding>
4085 +              <includes>
4086 +                <include>**/*.properties</include>
4087 +              </includes>
4088 +            </configuration>
4089 +          </execution>
4090 +        </executions>
4091 +      </plugin>
4092 +
4093        <plugin>
4094          <groupId>com.github.wvengen</groupId>
4095          <artifactId>proguard-maven-plugin</artifactId>
4096          <version>2.0.14</version>
4097          <executions>
4098 -         <execution>
4099 -           <phase>package</phase>
4100 -           <goals>
4101 -             <goal>proguard</goal>
4102 -           </goals>
4103 -         </execution>
4104 +          <execution>
4105 +            <phase>package</phase>
4106 +            <goals><goal>proguard</goal></goals>
4107 +          </execution>
4108          </executions>
4109          <dependencies>
4110            <dependency>
4111 @@ -109,7 +136,7 @@
4112            <addMavenDescriptor>false</addMavenDescriptor>
4113          </configuration>
4114        </plugin>
4115 -      -->
4116 +
4117        <plugin>
4118          <groupId>org.apache.maven.plugins</groupId>
4119          <artifactId>maven-jar-plugin</artifactId>
4120 @@ -130,40 +157,6 @@
4121            </archive>
4122          </configuration>
4123        </plugin>
4124 -
4125 -      <plugin>
4126 -        <groupId>org.apache.maven.plugins</groupId>
4127 -        <artifactId>maven-shade-plugin</artifactId>
4128 -        <version>3.2.1</version>
4129 -        <configuration>
4130 -          <!-- put your configurations here -->
4131 -        </configuration>
4132 -        <executions>
4133 -          <execution>
4134 -            <phase>package</phase>
4135 -            <goals>
4136 -              <goal>shade</goal>
4137 -            </goals>
4138 -            <!--
4139 -            <configuration>
4140 -              <minimizeJar>true</minimizeJar>
4141 -              <filters>
4142 -                <filter>
4143 -                  <artifact>install4j-runtime</artifact>
4144 -                  <includes>
4145 -                    <include>**</include>
4146 -                  </includes>
4147 -                </filter>
4148 -              </filters>
4149 -            </configuration>
4150 -            -->
4151 -          </execution>
4152 -        </executions>
4153 -      </plugin>
4154 -
4155 -
4156 -
4157 -
4158      </plugins>
4159    </build>
4160  
4161 @@ -202,6 +195,7 @@
4162                  <lib>${java.home}/jmods/java.base.jmod</lib>
4163                  <lib>${java.home}/jmods/java.desktop.jmod</lib>
4164                  <lib>${java.home}/jmods/java.logging.jmod</lib>
4165 +                <lib>${java.home}/jmods/java.scripting.jmod</lib>
4166                  <lib>${java.home}/jmods/jdk.jsobject.jmod</lib>
4167                </libs>
4168              </configuration>
4169 diff --git a/getdown/src/getdown/launcher/src/main/java/com/threerings/getdown/launcher/AbortPanel.java b/getdown/src/getdown/launcher/src/main/java/com/threerings/getdown/launcher/AbortPanel.java
4170 index dc1e54e46..4bd9f90b0 100644
4171 --- a/getdown/src/getdown/launcher/src/main/java/com/threerings/getdown/launcher/AbortPanel.java
4172 +++ b/getdown/src/getdown/launcher/src/main/java/com/threerings/getdown/launcher/AbortPanel.java
4173 @@ -72,7 +72,7 @@ public final class AbortPanel extends JFrame
4174      public void actionPerformed (ActionEvent e)
4175      {
4176          String cmd = e.getActionCommand();
4177 -        if (cmd.equals("ok")) {
4178 +        if ("ok".equals(cmd)) {
4179              System.exit(0);
4180          } else {
4181              setVisible(false);
4182 diff --git a/getdown/src/getdown/launcher/src/main/java/com/threerings/getdown/launcher/Getdown.java b/getdown/src/getdown/launcher/src/main/java/com/threerings/getdown/launcher/Getdown.java
4183 index 5750fce66..1cc6922c8 100644
4184 --- a/getdown/src/getdown/launcher/src/main/java/com/threerings/getdown/launcher/Getdown.java
4185 +++ b/getdown/src/getdown/launcher/src/main/java/com/threerings/getdown/launcher/Getdown.java
4186 @@ -9,51 +9,73 @@ import java.awt.BorderLayout;
4187  import java.awt.Container;
4188  import java.awt.Dimension;
4189  import java.awt.EventQueue;
4190 -import java.awt.Graphics;
4191  import java.awt.GraphicsEnvironment;
4192  import java.awt.Image;
4193  import java.awt.event.ActionEvent;
4194  import java.awt.image.BufferedImage;
4195  import java.io.BufferedReader;
4196  import java.io.File;
4197 -import java.io.FileInputStream;
4198  import java.io.FileNotFoundException;
4199 -import java.io.FileReader;
4200  import java.io.IOException;
4201  import java.io.InputStream;
4202  import java.io.InputStreamReader;
4203  import java.io.PrintStream;
4204  import java.net.HttpURLConnection;
4205 -import java.net.MalformedURLException;
4206  import java.net.URL;
4207 -import java.util.*;
4208 +import java.util.ArrayList;
4209 +import java.util.Arrays;
4210 +import java.util.Collection;
4211 +import java.util.HashSet;
4212 +import java.util.List;
4213 +import java.util.Locale;
4214 +import java.util.ResourceBundle;
4215 +import java.util.Set;
4216 +import java.util.concurrent.TimeUnit;
4217  
4218  import javax.imageio.ImageIO;
4219  import javax.swing.AbstractAction;
4220  import javax.swing.JButton;
4221  import javax.swing.JFrame;
4222  import javax.swing.JLayeredPane;
4223 -import javax.swing.JPanel;
4224  
4225  import com.samskivert.swing.util.SwingUtil;
4226 -import com.threerings.getdown.data.*;
4227 +import com.threerings.getdown.data.Application;
4228  import com.threerings.getdown.data.Application.UpdateInterface.Step;
4229 +import com.threerings.getdown.data.Build;
4230 +import com.threerings.getdown.data.EnvConfig;
4231 +import com.threerings.getdown.data.Resource;
4232 +import com.threerings.getdown.data.SysProps;
4233  import com.threerings.getdown.net.Downloader;
4234 -import com.threerings.getdown.net.HTTPDownloader;
4235  import com.threerings.getdown.tools.Patcher;
4236 -import com.threerings.getdown.util.*;
4237 -
4238 +import com.threerings.getdown.util.Config;
4239 +import com.threerings.getdown.util.FileUtil;
4240 +import com.threerings.getdown.util.LaunchUtil;
4241 +import com.threerings.getdown.util.MessageUtil;
4242 +import com.threerings.getdown.util.ProgressAggregator;
4243 +import com.threerings.getdown.util.ProgressObserver;
4244 +import com.threerings.getdown.util.StringUtil;
4245 +import com.threerings.getdown.util.VersionUtil;
4246  import static com.threerings.getdown.Log.log;
4247  
4248  /**
4249   * Manages the main control for the Getdown application updater and deployment system.
4250   */
4251 -public abstract class Getdown extends Thread
4252 +public abstract class Getdown
4253      implements Application.StatusDisplay, RotatingBackgrounds.ImageLoader
4254  {
4255 +    /**
4256 +     * Starts a thread to run Getdown and ultimately (hopefully) launch the target app.
4257 +     */
4258 +    public static void run (final Getdown getdown) {
4259 +        new Thread("Getdown") {
4260 +            @Override public void run () {
4261 +                getdown.run();
4262 +            }
4263 +        }.start();
4264 +    }
4265 +
4266      public Getdown (EnvConfig envc)
4267      {
4268 -        super("Getdown");
4269          try {
4270              // If the silent property exists, install without bringing up any gui. If it equals
4271              // launch, start the application after installing. Otherwise, just install and exit.
4272 @@ -80,7 +102,7 @@ public abstract class Getdown extends Thread
4273              // welcome to hell, where java can't cope with a classpath that contains jars that live
4274              // in a directory that contains a !, at least the same bug happens on all platforms
4275              String dir = envc.appDir.toString();
4276 -            if (dir.equals(".")) {
4277 +            if (".".equals(dir)) {
4278                  dir = System.getProperty("user.dir");
4279              }
4280              String errmsg = "The directory in which this application is installed:\n" + dir +
4281 @@ -107,7 +129,7 @@ public abstract class Getdown extends Thread
4282      {
4283          if (SysProps.noInstall()) {
4284              log.info("Skipping install due to 'no_install' sysprop.");
4285 -        } else if (_readyToInstall) {
4286 +        } else if (isUpdateAvailable()) {
4287              log.info("Installing " + _toInstallResources.size() + " downloaded resources:");
4288              for (Resource resource : _toInstallResources) {
4289                  resource.install(true);
4290 @@ -120,8 +142,28 @@ public abstract class Getdown extends Thread
4291          }
4292      }
4293  
4294 -    @Override
4295 -    public void run ()
4296 +    /**
4297 +     * Configures our proxy settings (called by {@link ProxyPanel}) and fires up the launcher.
4298 +     */
4299 +    public void configProxy (String host, String port, String username, String password)
4300 +    {
4301 +        log.info("User configured proxy", "host", host, "port", port);
4302 +        ProxyUtil.configProxy(_app, host, port, username, password);
4303 +
4304 +        // clear out our UI
4305 +        disposeContainer();
4306 +        _container = null;
4307 +
4308 +        // fire up a new thread
4309 +        run(this);
4310 +    }
4311 +
4312 +    /**
4313 +     * The main entry point of Getdown: does some sanity checks and preparation, then delegates the
4314 +     * actual getting down to {@link #getdown}. This is not called directly, but rather via the
4315 +     * static {@code run} method as Getdown does its main work on a separate thread.
4316 +     */
4317 +    protected void run ()
4318      {
4319          // if we have no messages, just bail because we're hosed; the error message will be
4320          // displayed to the user already
4321 @@ -135,95 +177,48 @@ public abstract class Getdown extends Thread
4322          File instdir = _app.getLocalPath("");
4323          if (!instdir.canWrite()) {
4324              String path = instdir.getPath();
4325 -            if (path.equals(".")) {
4326 +            if (".".equals(path)) {
4327                  path = System.getProperty("user.dir");
4328              }
4329              fail(MessageUtil.tcompose("m.readonly_error", path));
4330              return;
4331          }
4332  
4333 -        try {
4334 -            _dead = false;
4335 -            // if we fail to detect a proxy, but we're allowed to run offline, then go ahead and
4336 -            // run the app anyway because we're prepared to cope with not being able to update
4337 -            if (detectProxy() || _app.allowOffline()) {
4338 -                getdown();
4339 -            } else if (_silent) {
4340 -                log.warning("Need a proxy, but we don't want to bother anyone.  Exiting.");
4341 -            } else {
4342 -                // create a panel they can use to configure the proxy settings
4343 -                _container = createContainer();
4344 -                // allow them to close the window to abort the proxy configuration
4345 -                _dead = true;
4346 -                configureContainer();
4347 -                ProxyPanel panel = new ProxyPanel(this, _msgs);
4348 -                // set up any existing configured proxy
4349 -                String[] hostPort = ProxyUtil.loadProxy(_app);
4350 -                panel.setProxy(hostPort[0], hostPort[1]);
4351 -                _container.add(panel, BorderLayout.CENTER);
4352 -                showContainer();
4353 -            }
4354 -
4355 -        } catch (Exception e) {
4356 -            log.warning("run() failed.", e);
4357 -            String msg = e.getMessage();
4358 -            if (msg == null) {
4359 -                msg = MessageUtil.compose("m.unknown_error", _ifc.installError);
4360 -            } else if (!msg.startsWith("m.")) {
4361 -                // try to do something sensible based on the type of error
4362 -                if (e instanceof FileNotFoundException) {
4363 -                    msg = MessageUtil.compose(
4364 -                        "m.missing_resource", MessageUtil.taint(msg), _ifc.installError);
4365 -                } else {
4366 -                    msg = MessageUtil.compose(
4367 -                        "m.init_error", MessageUtil.taint(msg), _ifc.installError);
4368 -                }
4369 -            }
4370 -            fail(msg);
4371 -        }
4372 -    }
4373 -
4374 -    /**
4375 -     * Configures our proxy settings (called by {@link ProxyPanel}) and fires up the launcher.
4376 -     */
4377 -    public void configProxy (String host, String port, String username, String password)
4378 -    {
4379 -        log.info("User configured proxy", "host", host, "port", port);
4380 -
4381 -        if (!StringUtil.isBlank(host)) {
4382 -            ProxyUtil.configProxy(_app, host, port, username, password);
4383 -        }
4384 -
4385 -        // clear out our UI
4386 -        disposeContainer();
4387 -        _container = null;
4388 -
4389 -        // fire up a new thread
4390 -        new Thread(this).start();
4391 +        _dead = false;
4392 +        // if we fail to detect a proxy, but we're allowed to run offline, then go ahead and
4393 +        // run the app anyway because we're prepared to cope with not being able to update
4394 +        if (detectProxy() || _app.allowOffline()) getdown();
4395 +        else requestProxyInfo(false);
4396      }
4397  
4398      protected boolean detectProxy () {
4399 -        if (ProxyUtil.autoDetectProxy(_app)) {
4400 -            return true;
4401 -        }
4402 -
4403 -        // otherwise see if we actually need a proxy; first we have to initialize our application
4404 -        // to get some sort of interface configuration and the appbase URL
4405 +        // first we have to initialize our application to get the appbase URL, etc.
4406          log.info("Checking whether we need to use a proxy...");
4407          try {
4408              readConfig(true);
4409          } catch (IOException ioe) {
4410              // no worries
4411          }
4412 +
4413 +        boolean tryNoProxy = SysProps.tryNoProxyFirst();
4414 +        if (!tryNoProxy && ProxyUtil.autoDetectProxy(_app)) {
4415 +            return true;
4416 +        }
4417 +
4418 +        // see if we actually need a proxy
4419          updateStatus("m.detecting_proxy");
4420 -        if (!ProxyUtil.canLoadWithoutProxy(_app.getConfigResource().getRemote())) {
4421 -            return false;
4422 +        URL configURL = _app.getConfigResource().getRemote();
4423 +        if (!ProxyUtil.canLoadWithoutProxy(configURL, tryNoProxy ? 2 : 5)) {
4424 +            // if we didn't auto-detect proxy first thing, do auto-detect now
4425 +            return tryNoProxy ? ProxyUtil.autoDetectProxy(_app) : false;
4426          }
4427  
4428 -        // we got through, so we appear not to require a proxy; make a blank proxy config so that
4429 -        // we don't go through this whole detection process again next time
4430          log.info("No proxy appears to be needed.");
4431 -        ProxyUtil.saveProxy(_app, null, null);
4432 +        if (!tryNoProxy)  {
4433 +            // we got through, so we appear not to require a proxy; make a blank proxy config so
4434 +            // that we don't go through this whole detection process again next time
4435 +            ProxyUtil.saveProxy(_app, null, null);
4436 +        }
4437          return true;
4438      }
4439  
4440 @@ -233,6 +228,25 @@ public abstract class Getdown extends Thread
4441          _ifc = new Application.UpdateInterface(config);
4442      }
4443  
4444 +    protected void requestProxyInfo (boolean reinitAuth) {
4445 +        if (_silent) {
4446 +            log.warning("Need a proxy, but we don't want to bother anyone. Exiting.");
4447 +            return;
4448 +        }
4449 +
4450 +        // create a panel they can use to configure the proxy settings
4451 +        _container = createContainer();
4452 +        // allow them to close the window to abort the proxy configuration
4453 +        _dead = true;
4454 +        configureContainer();
4455 +        ProxyPanel panel = new ProxyPanel(this, _msgs, reinitAuth);
4456 +        // set up any existing configured proxy
4457 +        String[] hostPort = ProxyUtil.loadProxy(_app);
4458 +        panel.setProxy(hostPort[0], hostPort[1]);
4459 +        _container.add(panel, BorderLayout.CENTER);
4460 +        showContainer();
4461 +    }
4462 +
4463      /**
4464       * Downloads and installs (without verifying) any resources that are marked with a
4465       * {@code PRELOAD} attribute.
4466 @@ -247,13 +261,15 @@ public abstract class Getdown extends Thread
4467              }
4468          }
4469  
4470 -        try {
4471 -            download(predownloads);
4472 -            for (Resource rsrc : predownloads) {
4473 -                rsrc.install(false); // install but don't validate yet
4474 +        if (!predownloads.isEmpty()) {
4475 +            try {
4476 +                download(predownloads);
4477 +                for (Resource rsrc : predownloads) {
4478 +                    rsrc.install(false); // install but don't validate yet
4479 +                }
4480 +            } catch (IOException ioe) {
4481 +                log.warning("Failed to predownload resources. Continuing...", ioe);
4482              }
4483 -        } catch (IOException ioe) {
4484 -            log.warning("Failed to predownload resources. Continuing...", ioe);
4485          }
4486      }
4487  
4488 @@ -263,7 +279,7 @@ public abstract class Getdown extends Thread
4489      protected void getdown ()
4490      {
4491          try {
4492 -            // first parses our application deployment file
4493 +            // first parse our application deployment file
4494              try {
4495                  readConfig(true);
4496              } catch (IOException ioe) {
4497 @@ -278,7 +294,7 @@ public abstract class Getdown extends Thread
4498                  throw new MultipleGetdownRunning();
4499              }
4500  
4501 -            // Update the config modtime so a sleeping getdown will notice the change.
4502 +            // update the config modtime so a sleeping getdown will notice the change
4503              File config = _app.getLocalPath(Application.CONFIG_FILE);
4504              if (!config.setLastModified(System.currentTimeMillis())) {
4505                  log.warning("Unable to set modtime on config file, will be unable to check for " +
4506 @@ -290,7 +306,7 @@ public abstract class Getdown extends Thread
4507                  // Store the config modtime before waiting the delay amount of time
4508                  long lastConfigModtime = config.lastModified();
4509                  log.info("Waiting " + _delay + " minutes before beginning actual work.");
4510 -                Thread.sleep(_delay * 60 * 1000);
4511 +                TimeUnit.MINUTES.sleep(_delay);
4512                  if (lastConfigModtime < config.lastModified()) {
4513                      log.warning("getdown.txt was modified while getdown was waiting.");
4514                      throw new MultipleGetdownRunning();
4515 @@ -339,11 +355,7 @@ public abstract class Getdown extends Thread
4516  
4517                  if (toDownload.size() > 0) {
4518                      // we have resources to download, also note them as to-be-installed
4519 -                    for (Resource r : toDownload) {
4520 -                        if (!_toInstallResources.contains(r)) {
4521 -                            _toInstallResources.add(r);
4522 -                        }
4523 -                    }
4524 +                    _toInstallResources.addAll(toDownload);
4525  
4526                      try {
4527                          // if any of our resources have already been marked valid this is not a
4528 @@ -427,24 +439,20 @@ public abstract class Getdown extends Thread
4529              throw new IOException("m.unable_to_repair");
4530  
4531          } catch (Exception e) {
4532 -            log.warning("getdown() failed.", e);
4533 -            String msg = e.getMessage();
4534 -            if (msg == null) {
4535 -                msg = MessageUtil.compose("m.unknown_error", _ifc.installError);
4536 -            } else if (!msg.startsWith("m.")) {
4537 -                // try to do something sensible based on the type of error
4538 -                if (e instanceof FileNotFoundException) {
4539 -                    msg = MessageUtil.compose(
4540 -                        "m.missing_resource", MessageUtil.taint(msg), _ifc.installError);
4541 -                } else {
4542 -                    msg = MessageUtil.compose(
4543 -                        "m.init_error", MessageUtil.taint(msg), _ifc.installError);
4544 -                }
4545 +            // if we failed due to proxy errors, ask for proxy info
4546 +            switch (_app.conn.state) {
4547 +            case NEED_PROXY:
4548 +                requestProxyInfo(false);
4549 +                break;
4550 +            case NEED_PROXY_AUTH:
4551 +                requestProxyInfo(true);
4552 +                break;
4553 +            default:
4554 +                log.warning("getdown() failed.", e);
4555 +                fail(e);
4556 +                _app.releaseLock();
4557 +                break;
4558              }
4559 -            // Since we're dead, clear off the 'time remaining' label along with displaying the
4560 -            // error message
4561 -            fail(msg);
4562 -            _app.releaseLock();
4563          }
4564      }
4565  
4566 @@ -499,6 +507,18 @@ public abstract class Getdown extends Thread
4567              throw new IOException("m.java_download_failed");
4568          }
4569  
4570 +        // on Windows, if the local JVM is in use, we will not be able to replace it with an
4571 +        // updated JVM; we detect this by attempting to rename the java.dll to its same name, which
4572 +        // will fail on Windows for in use files; hackery!
4573 +        File javaLocalDir = _app.getJavaLocalDir();
4574 +        File javaDll = new File(javaLocalDir, "bin" + File.separator + "java.dll");
4575 +        if (javaDll.exists()) {
4576 +            if (!javaDll.renameTo(javaDll)) {
4577 +                log.info("Cannot update local Java VM as it is in use.");
4578 +                return;
4579 +            }
4580 +        }
4581 +
4582          reportTrackingEvent("jvm_start", -1);
4583  
4584          updateStatus("m.downloading_java");
4585 @@ -507,21 +527,24 @@ public abstract class Getdown extends Thread
4586          download(list);
4587  
4588          reportTrackingEvent("jvm_unpack", -1);
4589 -
4590          updateStatus("m.unpacking_java");
4591 -        vmjar.install(true);
4592 +        try {
4593 +            vmjar.install(true);
4594 +        } catch (IOException ioe) {
4595 +            throw new IOException("m.java_unpack_failed", ioe);
4596 +        }
4597  
4598          // these only run on non-Windows platforms, so we use Unix file separators
4599 -        String localJavaDir = LaunchUtil.LOCAL_JAVA_DIR + "/";
4600 -        FileUtil.makeExecutable(_app.getLocalPath(localJavaDir + "bin/java"));
4601 -        FileUtil.makeExecutable(_app.getLocalPath(localJavaDir + "lib/jspawnhelper"));
4602 -        FileUtil.makeExecutable(_app.getLocalPath(localJavaDir + "lib/amd64/jspawnhelper"));
4603 +        FileUtil.makeExecutable(new File(javaLocalDir, "bin/java"));
4604 +        FileUtil.makeExecutable(new File(javaLocalDir, "lib/jspawnhelper"));
4605 +        FileUtil.makeExecutable(new File(javaLocalDir, "lib/amd64/jspawnhelper"));
4606  
4607          // lastly regenerate the .jsa dump file that helps Java to start up faster
4608 -        String vmpath = LaunchUtil.getJVMPath(_app.getLocalPath(""));
4609 +        String vmpath = LaunchUtil.getJVMBinaryPath(javaLocalDir, false);
4610 +        String[] command = { vmpath, "-Xshare:dump" };
4611          try {
4612              log.info("Regenerating classes.jsa for " + vmpath + "...");
4613 -            Runtime.getRuntime().exec(vmpath + " -Xshare:dump");
4614 +            Runtime.getRuntime().exec(command);
4615          } catch (Exception e) {
4616              log.warning("Failed to regenerate .jsa dump file", "error", e);
4617          }
4618 @@ -578,6 +601,8 @@ public abstract class Getdown extends Thread
4619              int ii = 0; for (Resource prsrc : list) {
4620                  ProgressObserver pobs = pragg.startElement(ii++);
4621                  try {
4622 +                    // if this patch file failed to download, skip it
4623 +                    if (!prsrc.getLocalNew().exists()) continue;
4624                      // install the patch file (renaming them from _new)
4625                      prsrc.install(false);
4626                      // now apply the patch
4627 @@ -613,7 +638,7 @@ public abstract class Getdown extends Thread
4628          // create our user interface
4629          createInterfaceAsync(false);
4630  
4631 -        Downloader dl = new HTTPDownloader(_app.proxy) {
4632 +        Downloader dl = new Downloader(_app.conn) {
4633              @Override protected void resolvingDownloads () {
4634                  updateStatus("m.resolving");
4635              }
4636 @@ -640,6 +665,10 @@ public abstract class Getdown extends Thread
4637                  log.warning("Download failed", "rsrc", rsrc, e);
4638              }
4639  
4640 +            @Override protected void resourceMissing (Resource rsrc) {
4641 +                log.warning("Resource missing (got 404)", "rsrc", rsrc);
4642 +            }
4643 +
4644              /** The last percentage at which we checked for another getdown running, or -1 for not
4645               * having checked at all. */
4646              protected int _lastCheck = -1;
4647 @@ -725,7 +754,7 @@ public abstract class Getdown extends Thread
4648              long minshow = _ifc.minShowSeconds * 1000L;
4649              if (_container != null && uptime < minshow) {
4650                  try {
4651 -                    Thread.sleep(minshow - uptime);
4652 +                    TimeUnit.MILLISECONDS.sleep(minshow - uptime);
4653                  } catch (Exception e) {
4654                  }
4655              }
4656 @@ -750,10 +779,9 @@ public abstract class Getdown extends Thread
4657          if (_silent || (_container != null && !reinit)) {
4658              return;
4659          }
4660 -/*
4661 +
4662          EventQueue.invokeLater(new Runnable() {
4663              public void run () {
4664 -*/
4665                  if (_container == null || reinit) {
4666                      if (_container == null) {
4667                          _container = createContainer();
4668 @@ -762,42 +790,6 @@ public abstract class Getdown extends Thread
4669                      }
4670                      configureContainer();
4671                      _layers = new JLayeredPane();
4672 -                    
4673 -                    
4674 -                    
4675 -                    // added in the instant display of a splashscreen
4676 -                    try {
4677 -                      readConfig(false);
4678 -                      Graphics g = _container.getGraphics();
4679 -                      String imageFile = _ifc.backgroundImage;
4680 -                      BufferedImage bgImage = loadImage(_ifc.backgroundImage);
4681 -                      int bwidth = bgImage.getWidth();
4682 -                      int bheight = bgImage.getHeight();
4683 -
4684 -                      instantSplashPane = new JPanel() {
4685 -                        @Override
4686 -                        protected void paintComponent(Graphics g)
4687 -                        {
4688 -                          super.paintComponent(g);
4689 -                          // attempt to draw a background image...
4690 -                          if (bgImage != null) {
4691 -                            g.drawImage(bgImage, 0, 0, this);
4692 -                          }
4693 -                        }
4694 -                      };
4695 -
4696 -                      instantSplashPane.setSize(bwidth,bheight);
4697 -                      instantSplashPane.setPreferredSize(new Dimension(bwidth,bheight));
4698 -
4699 -                      _layers.add(instantSplashPane, Integer.valueOf(0));
4700 -                    
4701 -                      _container.setPreferredSize(new Dimension(bwidth,bheight));
4702 -                    } catch (Exception e) {
4703 -                      log.warning("Failed to set instant background image", "bg", _ifc.backgroundImage);
4704 -                    }
4705 -
4706 -                    
4707 -                    
4708                      _container.add(_layers, BorderLayout.CENTER);
4709                      _patchNotes = new JButton(new AbstractAction(_msgs.getString("m.patch_notes")) {
4710                          @Override public void actionPerformed (ActionEvent event) {
4711 @@ -807,14 +799,12 @@ public abstract class Getdown extends Thread
4712                      _patchNotes.setFont(StatusPanel.FONT);
4713                      _layers.add(_patchNotes);
4714                      _status = new StatusPanel(_msgs);
4715 -                    _layers.add(_status, Integer.valueOf(10));
4716 +                    _layers.add(_status);
4717                      initInterface();
4718                  }
4719                  showContainer();
4720 -/*
4721              }
4722          });
4723 -*/
4724      }
4725  
4726      /**
4727 @@ -845,12 +835,13 @@ public abstract class Getdown extends Thread
4728  
4729      protected RotatingBackgrounds getBackground ()
4730      {
4731 -        if (_ifc.rotatingBackgrounds != null && _ifc.rotatingBackgrounds.size() > 0) {
4732 +        if (_ifc.rotatingBackgrounds != null) {
4733              if (_ifc.backgroundImage != null) {
4734                  log.warning("ui.background_image and ui.rotating_background were both specified. " +
4735 -                            "The background image is being used.");
4736 +                            "The rotating images are being used.");
4737              }
4738 -            return new RotatingBackgrounds(_ifc.rotatingBackgrounds, _ifc.errorBackground, Getdown.this);
4739 +            return new RotatingBackgrounds(_ifc.rotatingBackgrounds, _ifc.errorBackground,
4740 +                Getdown.this);
4741          } else if (_ifc.backgroundImage != null) {
4742              return new RotatingBackgrounds(loadImage(_ifc.backgroundImage));
4743          } else {
4744 @@ -879,8 +870,23 @@ public abstract class Getdown extends Thread
4745          }
4746      }
4747  
4748 +    private void fail (Exception e) {
4749 +        String msg = e.getMessage();
4750 +        if (msg == null) {
4751 +            msg = MessageUtil.compose("m.unknown_error", _ifc.installError);
4752 +        } else if (!msg.startsWith("m.")) {
4753 +            // try to do something sensible based on the type of error
4754 +            msg = MessageUtil.taint(msg);
4755 +            msg = e instanceof FileNotFoundException ?
4756 +                MessageUtil.compose("m.missing_resource", msg, _ifc.installError) :
4757 +                MessageUtil.compose("m.init_error", msg, _ifc.installError);
4758 +        }
4759 +        // since we're dead, clear off the 'time remaining' label along with displaying the error
4760 +        fail(msg);
4761 +    }
4762 +
4763      /**
4764 -     * Update the status to indicate getdown has failed for the reason in <code>message</code>.
4765 +     * Update the status to indicate getdown has failed for the reason in {@code message}.
4766       */
4767      protected void fail (String message)
4768      {
4769 @@ -961,14 +967,14 @@ public abstract class Getdown extends Thread
4770              do {
4771                  URL url = _app.getTrackingProgressURL(++_reportedProgress);
4772                  if (url != null) {
4773 -                    new ProgressReporter(url).start();
4774 +                    reportProgress(url);
4775                  }
4776              } while (_reportedProgress <= progress);
4777  
4778          } else {
4779              URL url = _app.getTrackingURL(event);
4780              if (url != null) {
4781 -                new ProgressReporter(url).start();
4782 +                reportProgress(url);
4783              }
4784          }
4785      }
4786 @@ -1031,44 +1037,40 @@ public abstract class Getdown extends Thread
4787      }
4788  
4789      /** Used to fetch a progress report URL. */
4790 -    protected class ProgressReporter extends Thread
4791 -    {
4792 -        public ProgressReporter (URL url) {
4793 -            setDaemon(true);
4794 -            _url = url;
4795 -        }
4796 -
4797 -        @Override
4798 -        public void run () {
4799 -            try {
4800 -                HttpURLConnection ucon = ConnectionUtil.openHttp(_app.proxy, _url, 0, 0);
4801 -
4802 -                // if we have a tracking cookie configured, configure the request with it
4803 -                if (_app.getTrackingCookieName() != null &&
4804 -                    _app.getTrackingCookieProperty() != null) {
4805 -                    String val = System.getProperty(_app.getTrackingCookieProperty());
4806 -                    if (val != null) {
4807 -                        ucon.setRequestProperty("Cookie", _app.getTrackingCookieName() + "=" + val);
4808 +    protected void reportProgress (final URL url) {
4809 +        Thread reporter = new Thread("Progress reporter") {
4810 +            public void run () {
4811 +                try {
4812 +                    HttpURLConnection ucon = _app.conn.openHttp(url, 0, 0);
4813 +
4814 +                    // if we have a tracking cookie configured, configure the request with it
4815 +                    if (_app.getTrackingCookieName() != null &&
4816 +                        _app.getTrackingCookieProperty() != null) {
4817 +                        String val = System.getProperty(_app.getTrackingCookieProperty());
4818 +                        if (val != null) {
4819 +                            ucon.setRequestProperty(
4820 +                                "Cookie", _app.getTrackingCookieName() + "=" + val);
4821 +                        }
4822                      }
4823 -                }
4824  
4825 -                // now request our tracking URL and ensure that we get a non-error response
4826 -                ucon.connect();
4827 -                try {
4828 -                    if (ucon.getResponseCode() != HttpURLConnection.HTTP_OK) {
4829 -                        log.warning("Failed to report tracking event",
4830 -                            "url", _url, "rcode", ucon.getResponseCode());
4831 +                    // now request our tracking URL and ensure that we get a non-error response
4832 +                    ucon.connect();
4833 +                    try {
4834 +                        if (ucon.getResponseCode() != HttpURLConnection.HTTP_OK) {
4835 +                            log.warning("Failed to report tracking event",
4836 +                                        "url", url, "rcode", ucon.getResponseCode());
4837 +                        }
4838 +                    } finally {
4839 +                        ucon.disconnect();
4840                      }
4841 -                } finally {
4842 -                    ucon.disconnect();
4843 -                }
4844  
4845 -            } catch (IOException ioe) {
4846 -                log.warning("Failed to report tracking event", "url", _url, "error", ioe);
4847 +                } catch (IOException ioe) {
4848 +                    log.warning("Failed to report tracking event", "url", url, "error", ioe);
4849 +                }
4850              }
4851 -        }
4852 -
4853 -        protected URL _url;
4854 +        };
4855 +        reporter.setDaemon(true);
4856 +        reporter.start();
4857      }
4858  
4859      /** Used to pass progress on to our user interface. */
4860 @@ -1084,7 +1086,6 @@ public abstract class Getdown extends Thread
4861      protected ResourceBundle _msgs;
4862      protected Container _container;
4863      protected JLayeredPane _layers;
4864 -    protected JPanel instantSplashPane;
4865      protected StatusPanel _status;
4866      protected JButton _patchNotes;
4867      protected AbortPanel _abort;
4868 @@ -1112,5 +1113,4 @@ public abstract class Getdown extends Thread
4869  
4870      protected static final int MAX_LOOPS = 5;
4871      protected static final long FALLBACK_CHECK_TIME = 1000L;
4872 -    
4873  }
4874 diff --git a/getdown/src/getdown/launcher/src/main/java/com/threerings/getdown/launcher/GetdownApp.java b/getdown/src/getdown/launcher/src/main/java/com/threerings/getdown/launcher/GetdownApp.java
4875 index 2d1908904..3859f6a09 100644
4876 --- a/getdown/src/getdown/launcher/src/main/java/com/threerings/getdown/launcher/GetdownApp.java
4877 +++ b/getdown/src/getdown/launcher/src/main/java/com/threerings/getdown/launcher/GetdownApp.java
4878 @@ -28,258 +28,225 @@ import javax.swing.KeyStroke;
4879  import javax.swing.WindowConstants;
4880  
4881  import com.samskivert.swing.util.SwingUtil;
4882 -import com.threerings.getdown.data.Application;
4883  import com.threerings.getdown.data.EnvConfig;
4884  import com.threerings.getdown.data.SysProps;
4885  import com.threerings.getdown.util.LaunchUtil;
4886  import com.threerings.getdown.util.StringUtil;
4887  import static com.threerings.getdown.Log.log;
4888 -import jalview.bin.StartupNotificationListener;
4889  
4890  /**
4891   * The main application entry point for Getdown.
4892   */
4893  public class GetdownApp
4894  {
4895 -  public static String startupFilesParameterString = "";
4896 -  /**
4897 -   * The main entry point of the Getdown launcher application.
4898 -   */
4899 -  public static void main (String[] argv) {
4900 -    try {
4901 -      start(argv);
4902 -    } catch (Exception e) {
4903 -      log.warning("main() failed.", e);
4904 -    }
4905 -  }
4906 -
4907 -  /**
4908 -   * Runs Getdown as an application, using the arguments supplie as {@code argv}.
4909 -   * @return the {@code Getdown} instance that is running. {@link Getdown#start} will have been
4910 -   * called on it.
4911 -   * @throws Exception if anything goes wrong starting Getdown.
4912 -   */
4913 -  public static Getdown start (String[] argv) throws Exception {
4914 -    List<EnvConfig.Note> notes = new ArrayList<>();
4915 -    EnvConfig envc = EnvConfig.create(argv, notes);
4916 -    if (envc == null) {
4917 -      if (!notes.isEmpty()) for (EnvConfig.Note n : notes) System.err.println(n.message);
4918 -      else System.err.println("Usage: java -jar getdown.jar [app_dir] [app_id] [app args]");
4919 -      System.exit(-1);
4920 -    }
4921 -
4922 -    // pipe our output into a file in the application directory
4923 -    if (!SysProps.noLogRedir()) {
4924 -      File logFile = new File(envc.appDir, "launcher.log");
4925 -      try {
4926 -        PrintStream logOut = new PrintStream(
4927 -                new BufferedOutputStream(new FileOutputStream(logFile)), true);
4928 -        System.setOut(logOut);
4929 -        System.setErr(logOut);
4930 -      } catch (IOException ioe) {
4931 -        log.warning("Unable to redirect output to '" + logFile + "': " + ioe);
4932 -      }
4933 -    }
4934 -
4935 -    // report any notes from reading our env config, and abort if necessary
4936 -    boolean abort = false;
4937 -    for (EnvConfig.Note note : notes) {
4938 -      switch (note.level) {
4939 -      case INFO: log.info(note.message); break;
4940 -      case WARN: log.warning(note.message); break;
4941 -      case ERROR: log.error(note.message); abort = true; break;
4942 -      }
4943 +    /**
4944 +     * The main entry point of the Getdown launcher application.
4945 +     */
4946 +    public static void main (String[] argv) {
4947 +        try {
4948 +            start(argv);
4949 +        } catch (Exception e) {
4950 +            log.warning("main() failed.", e);
4951 +        }
4952      }
4953 -    if (abort) System.exit(-1);
4954  
4955 -    try
4956 -    {
4957 -      jalview.bin.StartupNotificationListener.setListener();
4958 -    } catch (Exception e)
4959 -    {
4960 -      e.printStackTrace();
4961 -    } catch (NoClassDefFoundError e)
4962 -    {
4963 -      log.warning("Starting without install4j classes");
4964 -    } catch (Throwable t)
4965 -    {
4966 -      t.printStackTrace();
4967 -    }
4968 -    
4969 -    // record a few things for posterity
4970 -    log.info("------------------ VM Info ------------------");
4971 -    log.info("-- OS Name: " + System.getProperty("os.name"));
4972 -    log.info("-- OS Arch: " + System.getProperty("os.arch"));
4973 -    log.info("-- OS Vers: " + System.getProperty("os.version"));
4974 -    log.info("-- Java Vers: " + System.getProperty("java.version"));
4975 -    log.info("-- Java Home: " + System.getProperty("java.home"));
4976 -    log.info("-- User Name: " + System.getProperty("user.name"));
4977 -    log.info("-- User Home: " + System.getProperty("user.home"));
4978 -    log.info("-- Cur dir: " + System.getProperty("user.dir"));
4979 -    log.info("-- startupFilesParameterString: " + startupFilesParameterString);
4980 -    log.info("---------------------------------------------");
4981 +    /**
4982 +     * Runs Getdown as an application, using the arguments supplie as {@code argv}.
4983 +     * @return the {@code Getdown} instance that is running. {@link Getdown#run} will have been
4984 +     * called on it.
4985 +     * @throws Exception if anything goes wrong starting Getdown.
4986 +     */
4987 +    public static Getdown start (String[] argv) throws Exception {
4988 +        List<EnvConfig.Note> notes = new ArrayList<>();
4989 +        EnvConfig envc = EnvConfig.create(argv, notes);
4990 +        if (envc == null) {
4991 +            if (!notes.isEmpty()) for (EnvConfig.Note n : notes) System.err.println(n.message);
4992 +            else System.err.println("Usage: java -jar getdown.jar [app_dir] [app_id] [app args]");
4993 +            System.exit(-1);
4994 +        }
4995  
4996 -    Getdown app = new Getdown(envc) {
4997 -      @Override
4998 -      protected Container createContainer () {
4999 -        // create our user interface, and display it
5000 -        if (_frame == null) {
5001 -          _frame = new JFrame("");
5002 -          _frame.addWindowListener(new WindowAdapter() {
5003 -            @Override
5004 -            public void windowClosing (WindowEvent evt) {
5005 -              handleWindowClose();
5006 -            }
5007 -          });
5008 -          // handle close on ESC
5009 -          String cancelId = "Cancel"; // $NON-NLS-1$
5010 -          _frame.getRootPane().getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(
5011 -                  KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), cancelId);
5012 -          _frame.getRootPane().getActionMap().put(cancelId, new AbstractAction() {
5013 -            public void actionPerformed (ActionEvent e) {
5014 -              handleWindowClose();
5015 +        // pipe our output into a file in the application directory
5016 +        if (!SysProps.noLogRedir() && !SysProps.debug()) {
5017 +            File logFile = new File(envc.appDir, "launcher.log");
5018 +            try {
5019 +                PrintStream logOut = new PrintStream(
5020 +                    new BufferedOutputStream(new FileOutputStream(logFile)), true);
5021 +                System.setOut(logOut);
5022 +                System.setErr(logOut);
5023 +            } catch (IOException ioe) {
5024 +                log.warning("Unable to redirect output to '" + logFile + "': " + ioe);
5025              }
5026 -          });
5027 -          // this cannot be called in configureContainer as it is only allowed before the
5028 -          // frame has been displayed for the first time
5029 -          _frame.setUndecorated(_ifc.hideDecorations);
5030 -          _frame.setResizable(false);
5031 -        } else {
5032 -          _frame.getContentPane().removeAll();
5033          }
5034 -        _frame.setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE);
5035 -        return _frame.getContentPane();
5036 -      }
5037 -
5038 -      @Override
5039 -      protected void configureContainer () {
5040 -        if (_frame == null) return;
5041 -
5042 -        _frame.setTitle(_ifc.name);
5043  
5044 -        try {
5045 -          _frame.setBackground(new Color(_ifc.background, true));
5046 -        } catch (Exception e) {
5047 -          log.warning("Failed to set background", "bg", _ifc.background, e);
5048 +        // report any notes from reading our env config, and abort if necessary
5049 +        boolean abort = false;
5050 +        for (EnvConfig.Note note : notes) {
5051 +            switch (note.level) {
5052 +            case INFO: log.info(note.message); break;
5053 +            case WARN: log.warning(note.message); break;
5054 +            case ERROR: log.error(note.message); abort = true; break;
5055 +            }
5056          }
5057 +        if (abort) System.exit(-1);
5058 +
5059 +        // record a few things for posterity
5060 +        log.info("------------------ VM Info ------------------");
5061 +        log.info("-- OS Name: " + System.getProperty("os.name"));
5062 +        log.info("-- OS Arch: " + System.getProperty("os.arch"));
5063 +        log.info("-- OS Vers: " + System.getProperty("os.version"));
5064 +        log.info("-- Java Vers: " + System.getProperty("java.version"));
5065 +        log.info("-- Java Home: " + System.getProperty("java.home"));
5066 +        log.info("-- User Name: " + System.getProperty("user.name"));
5067 +        log.info("-- User Home: " + System.getProperty("user.home"));
5068 +        log.info("-- Cur dir: " + System.getProperty("user.dir"));
5069 +        log.info("---------------------------------------------");
5070 +
5071 +        Getdown getdown = new Getdown(envc) {
5072 +            @Override
5073 +            protected Container createContainer () {
5074 +                // create our user interface, and display it
5075 +                if (_frame == null) {
5076 +                    _frame = new JFrame("");
5077 +                    _frame.addWindowListener(new WindowAdapter() {
5078 +                        @Override
5079 +                        public void windowClosing (WindowEvent evt) {
5080 +                            handleWindowClose();
5081 +                        }
5082 +                    });
5083 +                    // handle close on ESC
5084 +                    String cancelId = "Cancel"; // $NON-NLS-1$
5085 +                    _frame.getRootPane().getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(
5086 +                        KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), cancelId);
5087 +                    _frame.getRootPane().getActionMap().put(cancelId, new AbstractAction() {
5088 +                        public void actionPerformed (ActionEvent e) {
5089 +                            handleWindowClose();
5090 +                        }
5091 +                    });
5092 +                    // this cannot be called in configureContainer as it is only allowed before the
5093 +                    // frame has been displayed for the first time
5094 +                    _frame.setUndecorated(_ifc.hideDecorations);
5095 +                    _frame.setResizable(false);
5096 +                } else {
5097 +                    _frame.getContentPane().removeAll();
5098 +                }
5099 +                _frame.setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE);
5100 +                return _frame.getContentPane();
5101 +            }
5102  
5103 -        if (_ifc.iconImages != null && _ifc.iconImages.size() > 0) {
5104 -          ArrayList<Image> icons = new ArrayList<>();
5105 -          for (String path : _ifc.iconImages) {
5106 -            Image img = loadImage(path);
5107 -            if (img == null) {
5108 -              log.warning("Error loading icon image", "path", path);
5109 -            } else {
5110 -              icons.add(img);
5111 +            @Override
5112 +            protected void configureContainer () {
5113 +                if (_frame == null) return;
5114 +
5115 +                _frame.setTitle(_ifc.name);
5116 +
5117 +                try {
5118 +                    _frame.setBackground(new Color(_ifc.background, true));
5119 +                } catch (Exception e) {
5120 +                    log.warning("Failed to set background", "bg", _ifc.background, e);
5121 +                }
5122 +
5123 +                if (_ifc.iconImages != null) {
5124 +                    List<Image> icons = new ArrayList<>();
5125 +                    for (String path : _ifc.iconImages) {
5126 +                        Image img = loadImage(path);
5127 +                        if (img == null) {
5128 +                            log.warning("Error loading icon image", "path", path);
5129 +                        } else {
5130 +                            icons.add(img);
5131 +                        }
5132 +                    }
5133 +                    if (icons.isEmpty()) {
5134 +                        log.warning("Failed to load any icons", "iconImages", _ifc.iconImages);
5135 +                    } else {
5136 +                        _frame.setIconImages(icons);
5137 +                    }
5138 +                }
5139              }
5140 -          }
5141 -          if (icons.isEmpty()) {
5142 -            log.warning("Failed to load any icons", "iconImages", _ifc.iconImages);
5143 -          } else {
5144 -            _frame.setIconImages(icons);
5145 -          }
5146 -        }
5147 -      }
5148  
5149 -      @Override
5150 -      protected void showContainer () {
5151 -        if (_frame != null) {
5152 -          _frame.pack();
5153 -          SwingUtil.centerWindow(_frame);
5154 -          _frame.setVisible(true);
5155 -        }
5156 -      }
5157 +            @Override
5158 +            protected void showContainer () {
5159 +                if (_frame != null) {
5160 +                    _frame.pack();
5161 +                    SwingUtil.centerWindow(_frame);
5162 +                    _frame.setVisible(true);
5163 +                }
5164 +            }
5165  
5166 -      @Override
5167 -      protected void disposeContainer () {
5168 -        if (_frame != null) {
5169 -          _frame.dispose();
5170 -          _frame = null;
5171 -        }
5172 -      }
5173 +            @Override
5174 +            protected void disposeContainer () {
5175 +                if (_frame != null) {
5176 +                    _frame.dispose();
5177 +                    _frame = null;
5178 +                }
5179 +            }
5180  
5181 -      @Override
5182 -      protected void showDocument (String url) {
5183 -        if (!StringUtil.couldBeValidUrl(url)) {
5184 -          // command injection would be possible if we allowed e.g. spaces and double quotes
5185 -          log.warning("Invalid document URL.", "url", url);
5186 -          return;
5187 -        }
5188 -        String[] cmdarray;
5189 -        if (LaunchUtil.isWindows()) {
5190 -          String osName = System.getProperty("os.name", "");
5191 -          if (osName.indexOf("9") != -1 || osName.indexOf("Me") != -1) {
5192 -            cmdarray = new String[] {
5193 -                "command.com", "/c", "start", "\"" + url + "\"" };
5194 -          } else {
5195 -            cmdarray = new String[] {
5196 -                "cmd.exe", "/c", "start", "\"\"", "\"" + url + "\"" };
5197 -          }
5198 -        } else if (LaunchUtil.isMacOS()) {
5199 -          cmdarray = new String[] { "open", url };
5200 -        } else { // Linux, Solaris, etc.
5201 -          cmdarray = new String[] { "firefox", url };
5202 -        }
5203 -        try {
5204 -          Runtime.getRuntime().exec(cmdarray);
5205 -        } catch (Exception e) {
5206 -          log.warning("Failed to open browser.", "cmdarray", cmdarray, e);
5207 -        }
5208 -      }
5209 +            @Override
5210 +            protected void showDocument (String url) {
5211 +                if (!StringUtil.couldBeValidUrl(url)) {
5212 +                    // command injection would be possible if we allowed e.g. spaces and double quotes
5213 +                    log.warning("Invalid document URL.", "url", url);
5214 +                    return;
5215 +                }
5216 +                String[] cmdarray;
5217 +                if (LaunchUtil.isWindows()) {
5218 +                    String osName = System.getProperty("os.name", "");
5219 +                    if (osName.contains("9") || osName.contains("Me")) {
5220 +                        cmdarray = new String[] {
5221 +                            "command.com", "/c", "start", "\"" + url + "\"" };
5222 +                    } else {
5223 +                        cmdarray = new String[] {
5224 +                            "cmd.exe", "/c", "start", "\"\"", "\"" + url + "\"" };
5225 +                    }
5226 +                } else if (LaunchUtil.isMacOS()) {
5227 +                    cmdarray = new String[] { "open", url };
5228 +                } else { // Linux, Solaris, etc.
5229 +                    cmdarray = new String[] { "firefox", url };
5230 +                }
5231 +                try {
5232 +                    Runtime.getRuntime().exec(cmdarray);
5233 +                } catch (Exception e) {
5234 +                    log.warning("Failed to open browser.", "cmdarray", cmdarray, e);
5235 +                }
5236 +            }
5237  
5238 -      @Override
5239 -      protected void exit (int exitCode) {
5240 -        // if we're running the app in the same JVM, don't call System.exit, but do
5241 -        // make double sure that the download window is closed.
5242 -        if (invokeDirect()) {
5243 -          disposeContainer();
5244 -        } else {
5245 -          System.exit(exitCode);
5246 -        }
5247 -      }
5248 +            @Override
5249 +            protected void exit (int exitCode) {
5250 +                // if we're running the app in the same JVM, don't call System.exit, but do
5251 +                // make double sure that the download window is closed.
5252 +                if (invokeDirect()) {
5253 +                    disposeContainer();
5254 +                } else {
5255 +                    System.exit(exitCode);
5256 +                }
5257 +            }
5258  
5259 -      @Override
5260 -      protected void fail (String message) {
5261 -        super.fail(message);
5262 -        // super.fail causes the UI to be created (if needed) on the next UI tick, so we
5263 -        // want to wait until that happens before we attempt to redecorate the window
5264 -        EventQueue.invokeLater(new Runnable() {
5265 -          @Override
5266 -          public void run() {
5267 -            // if the frame was set to be undecorated, make window decoration available
5268 -            // to allow the user to close the window
5269 -            if (_frame != null && _frame.isUndecorated()) {
5270 -              _frame.dispose();
5271 -              Color bg = _frame.getBackground();
5272 -              if (bg != null && bg.getAlpha() < 255) {
5273 -                // decorated windows do not allow alpha backgrounds
5274 -                _frame.setBackground(
5275 -                        new Color(bg.getRed(), bg.getGreen(), bg.getBlue()));
5276 -              }
5277 -              _frame.setUndecorated(false);
5278 -              showContainer();
5279 +            @Override
5280 +            protected void fail (String message) {
5281 +                super.fail(message);
5282 +                // super.fail causes the UI to be created (if needed) on the next UI tick, so we
5283 +                // want to wait until that happens before we attempt to redecorate the window
5284 +                EventQueue.invokeLater(new Runnable() {
5285 +                    @Override public void run () {
5286 +                        // if the frame was set to be undecorated, make window decoration available
5287 +                        // to allow the user to close the window
5288 +                        if (_frame != null && _frame.isUndecorated()) {
5289 +                            _frame.dispose();
5290 +                            Color bg = _frame.getBackground();
5291 +                            if (bg != null && bg.getAlpha() < 255) {
5292 +                                // decorated windows do not allow alpha backgrounds
5293 +                                _frame.setBackground(
5294 +                                    new Color(bg.getRed(), bg.getGreen(), bg.getBlue()));
5295 +                            }
5296 +                            _frame.setUndecorated(false);
5297 +                            showContainer();
5298 +                        }
5299 +                    }
5300 +                });
5301              }
5302 -          }
5303 -        });
5304 -      }
5305  
5306 -      protected JFrame _frame;
5307 -    };
5308 -    
5309 -    String startupFile = getStartupFilesParameterString();
5310 -    if (!StringUtil.isBlank(startupFile)) {
5311 -      Application.setStartupFilesFromParameterString(startupFile);
5312 +            protected JFrame _frame;
5313 +        };
5314 +        Getdown.run(getdown);
5315 +        return getdown;
5316      }
5317
5318 -    app.start();
5319 -    return app;
5320 -  }
5321 -  
5322 -  public static void setStartupFilesParameterString(String parameters) {
5323 -    startupFilesParameterString = parameters;
5324 -  }
5325 -  
5326 -  public static String getStartupFilesParameterString() {
5327 -    return startupFilesParameterString;
5328 -  }
5329  }
5330 diff --git a/getdown/src/getdown/launcher/src/main/java/com/threerings/getdown/launcher/ProxyPanel.java b/getdown/src/getdown/launcher/src/main/java/com/threerings/getdown/launcher/ProxyPanel.java
5331 index 217827364..5f18896c4 100644
5332 --- a/getdown/src/getdown/launcher/src/main/java/com/threerings/getdown/launcher/ProxyPanel.java
5333 +++ b/getdown/src/getdown/launcher/src/main/java/com/threerings/getdown/launcher/ProxyPanel.java
5334 @@ -35,14 +35,16 @@ import static com.threerings.getdown.Log.log;
5335   */
5336  public final class ProxyPanel extends JPanel implements ActionListener
5337  {
5338 -    public ProxyPanel (Getdown getdown, ResourceBundle msgs)
5339 +    public ProxyPanel (Getdown getdown, ResourceBundle msgs, boolean updateAuth)
5340      {
5341          _getdown = getdown;
5342          _msgs = msgs;
5343 +        _updateAuth = updateAuth;
5344  
5345          setLayout(new VGroupLayout());
5346          setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
5347 -        add(new SaneLabelField(get("m.configure_proxy")));
5348 +        String title = get(updateAuth ? "m.update_proxy_auth" : "m.configure_proxy");
5349 +        add(new SaneLabelField(title));
5350          add(new Spacer(5, 5));
5351  
5352          JPanel row = new JPanel(new GridLayout());
5353 @@ -61,19 +63,20 @@ public final class ProxyPanel extends JPanel implements ActionListener
5354          row.add(new SaneLabelField(get("m.proxy_auth_required")), BorderLayout.WEST);
5355          _useAuth = new JCheckBox();
5356          row.add(_useAuth);
5357 +        _useAuth.setSelected(updateAuth);
5358          add(row);
5359  
5360          row = new JPanel(new GridLayout());
5361          row.add(new SaneLabelField(get("m.proxy_username")), BorderLayout.WEST);
5362          _username = new SaneTextField();
5363 -        _username.setEnabled(false);
5364 +        _username.setEnabled(updateAuth);
5365          row.add(_username);
5366          add(row);
5367  
5368          row = new JPanel(new GridLayout());
5369          row.add(new SaneLabelField(get("m.proxy_password")), BorderLayout.WEST);
5370          _password = new SanePasswordField();
5371 -        _password.setEnabled(false);
5372 +        _password.setEnabled(updateAuth);
5373          row.add(_password);
5374          add(row);
5375  
5376 @@ -112,7 +115,13 @@ public final class ProxyPanel extends JPanel implements ActionListener
5377      public void addNotify ()
5378      {
5379          super.addNotify();
5380 -        _host.requestFocusInWindow();
5381 +        if (_updateAuth) {
5382 +            // we are asking the user to update the credentials for an existing proxy
5383 +            // configuration, so focus that instead of the proxy host config
5384 +            _username.requestFocusInWindow();
5385 +        } else {
5386 +            _host.requestFocusInWindow();
5387 +        }
5388      }
5389  
5390      // documentation inherited
5391 @@ -131,7 +140,7 @@ public final class ProxyPanel extends JPanel implements ActionListener
5392      public void actionPerformed (ActionEvent e)
5393      {
5394          String cmd = e.getActionCommand();
5395 -        if (cmd.equals("ok")) {
5396 +        if ("ok".equals(cmd)) {
5397              String user = null, pass = null;
5398              if (_useAuth.isSelected()) {
5399                  user = _username.getText();
5400 @@ -184,8 +193,9 @@ public final class ProxyPanel extends JPanel implements ActionListener
5401          return dim;
5402      }
5403  
5404 -    protected Getdown _getdown;
5405 -    protected ResourceBundle _msgs;
5406 +    protected final Getdown _getdown;
5407 +    protected final ResourceBundle _msgs;
5408 +    protected final boolean _updateAuth;
5409  
5410      protected JTextField _host;
5411      protected JTextField _port;
5412 diff --git a/getdown/src/getdown/launcher/src/main/java/com/threerings/getdown/launcher/ProxyUtil.java b/getdown/src/getdown/launcher/src/main/java/com/threerings/getdown/launcher/ProxyUtil.java
5413 index a36b5fa67..8962d35b9 100644
5414 --- a/getdown/src/getdown/launcher/src/main/java/com/threerings/getdown/launcher/ProxyUtil.java
5415 +++ b/getdown/src/getdown/launcher/src/main/java/com/threerings/getdown/launcher/ProxyUtil.java
5416 @@ -8,31 +8,41 @@ package com.threerings.getdown.launcher;
5417  import java.io.File;
5418  import java.io.FileOutputStream;
5419  import java.io.IOException;
5420 +import java.io.InputStreamReader;
5421  import java.io.PrintStream;
5422 +import java.io.Reader;
5423  import java.net.Authenticator;
5424  import java.net.HttpURLConnection;
5425 +import java.net.InetAddress;
5426  import java.net.InetSocketAddress;
5427  import java.net.PasswordAuthentication;
5428  import java.net.Proxy;
5429  import java.net.URL;
5430  import java.net.URLConnection;
5431 +import java.net.UnknownHostException;
5432  import java.util.Iterator;
5433  import java.util.ServiceLoader;
5434  
5435 +import javax.script.Bindings;
5436 +import javax.script.Invocable;
5437 +import javax.script.ScriptContext;
5438 +import javax.script.ScriptEngine;
5439 +import javax.script.ScriptEngineManager;
5440 +
5441  import ca.beq.util.win32.registry.RegistryKey;
5442  import ca.beq.util.win32.registry.RegistryValue;
5443  import ca.beq.util.win32.registry.RootKey;
5444  
5445  import com.threerings.getdown.data.Application;
5446 +import com.threerings.getdown.net.Connector;
5447  import com.threerings.getdown.spi.ProxyAuth;
5448  import com.threerings.getdown.util.Config;
5449 -import com.threerings.getdown.util.ConnectionUtil;
5450  import com.threerings.getdown.util.LaunchUtil;
5451  import com.threerings.getdown.util.StringUtil;
5452  
5453  import static com.threerings.getdown.Log.log;
5454  
5455 -public class ProxyUtil {
5456 +public final class ProxyUtil {
5457  
5458      public static boolean autoDetectProxy (Application app)
5459      {
5460 @@ -57,25 +67,40 @@ public class ProxyUtil {
5461                  RegistryKey r = new RegistryKey(RootKey.HKEY_CURRENT_USER, PROXY_REGISTRY);
5462                  for (Iterator<?> iter = r.values(); iter.hasNext(); ) {
5463                      RegistryValue value = (RegistryValue)iter.next();
5464 -                    if (value.getName().equals("ProxyEnable")) {
5465 -                        enabled = value.getStringValue().equals("1");
5466 +                    if ("ProxyEnable".equals(value.getName())) {
5467 +                        enabled = "1".equals(value.getStringValue());
5468                      }
5469                      if (value.getName().equals("ProxyServer")) {
5470 -                        String strval = value.getStringValue();
5471 -                        int cidx = strval.indexOf(":");
5472 -                        if (cidx != -1) {
5473 -                            rport = strval.substring(cidx+1);
5474 -                            strval = strval.substring(0, cidx);
5475 +                        String[] hostPort = splitHostPort(value.getStringValue());
5476 +                        rhost = hostPort[0];
5477 +                        rport = hostPort[1];
5478 +                    }
5479 +                    if (value.getName().equals("AutoConfigURL")) {
5480 +                        String acurl = value.getStringValue();
5481 +                        Reader acjs = new InputStreamReader(new URL(acurl).openStream());
5482 +                        // technically we should be returning all this info and trying each proxy
5483 +                        // in succession, but that's complexity we'll leave for another day
5484 +                        URL configURL = app.getConfigResource().getRemote();
5485 +                        for (String proxy : findPACProxiesForURL(acjs, configURL)) {
5486 +                            if (proxy.startsWith("PROXY ")) {
5487 +                                String[] hostPort = splitHostPort(proxy.substring(6));
5488 +                                rhost = hostPort[0];
5489 +                                rport = hostPort[1];
5490 +                                // TODO: is this valid? Does AutoConfigURL imply proxy enabled?
5491 +                                enabled = true;
5492 +                                break;
5493 +                            }
5494                          }
5495 -                        rhost = strval;
5496                      }
5497                  }
5498 +
5499                  if (enabled) {
5500                      host = rhost;
5501                      port = rport;
5502                  } else {
5503                      log.info("Detected no proxy settings in the registry.");
5504                  }
5505 +
5506              } catch (Throwable t) {
5507                  log.info("Failed to find proxy settings in Windows registry", "error", t);
5508              }
5509 @@ -97,32 +122,31 @@ public class ProxyUtil {
5510          return true;
5511      }
5512  
5513 -    public static boolean canLoadWithoutProxy (URL rurl)
5514 +    public static boolean canLoadWithoutProxy (URL rurl, int timeoutSeconds)
5515      {
5516 -        log.info("Testing whether proxy is needed, via: " + rurl);
5517 +        log.info("Attempting to fetch without proxy: " + rurl);
5518          try {
5519 -            // try to make a HEAD request for this URL (use short connect and read timeouts)
5520 -            URLConnection conn = ConnectionUtil.open(Proxy.NO_PROXY, rurl, 5, 5);
5521 -            if (conn instanceof HttpURLConnection) {
5522 -                HttpURLConnection hcon = (HttpURLConnection)conn;
5523 -                try {
5524 -                    hcon.setRequestMethod("HEAD");
5525 -                    hcon.connect();
5526 -                    // make sure we got a satisfactory response code
5527 -                    int rcode = hcon.getResponseCode();
5528 -                    if (rcode == HttpURLConnection.HTTP_PROXY_AUTH ||
5529 -                        rcode == HttpURLConnection.HTTP_FORBIDDEN) {
5530 -                        log.warning("Got an 'HTTP credentials needed' response", "code", rcode);
5531 -                    } else {
5532 -                        return true;
5533 -                    }
5534 -                } finally {
5535 -                    hcon.disconnect();
5536 -                }
5537 -            } else {
5538 -                // if the appbase is not an HTTP/S URL (like file:), then we don't need a proxy
5539 +            URLConnection conn = Connector.DEFAULT.open(rurl, timeoutSeconds, timeoutSeconds);
5540 +            // if the appbase is not an HTTP/S URL (like file:), then we don't need a proxy
5541 +            if (!(conn instanceof HttpURLConnection)) {
5542                  return true;
5543              }
5544 +            // otherwise, try to make a HEAD request for this URL
5545 +            HttpURLConnection hcon = (HttpURLConnection)conn;
5546 +            try {
5547 +                hcon.setRequestMethod("HEAD");
5548 +                hcon.connect();
5549 +                // make sure we got a satisfactory response code
5550 +                int rcode = hcon.getResponseCode();
5551 +                if (rcode == HttpURLConnection.HTTP_PROXY_AUTH ||
5552 +                    rcode == HttpURLConnection.HTTP_FORBIDDEN) {
5553 +                    log.warning("Got an 'HTTP credentials needed' response", "code", rcode);
5554 +                } else {
5555 +                    return true;
5556 +                }
5557 +            } finally {
5558 +                hcon.disconnect();
5559 +            }
5560          } catch (IOException ioe) {
5561              log.info("Failed to HEAD " + rurl + ": " + ioe);
5562              log.info("We probably need a proxy, but auto-detection failed.");
5563 @@ -190,9 +214,14 @@ public class ProxyUtil {
5564          }
5565          boolean haveCreds = !StringUtil.isBlank(username) && !StringUtil.isBlank(password);
5566  
5567 -        int pport = StringUtil.isBlank(port) ? 80 : Integer.valueOf(port);
5568 -        log.info("Using proxy", "host", host, "port", pport, "haveCreds", haveCreds);
5569 -        app.proxy = new Proxy(Proxy.Type.HTTP, new InetSocketAddress(host, pport));
5570 +        if (StringUtil.isBlank(host)) {
5571 +            log.info("Using no proxy");
5572 +            app.conn = new Connector(Proxy.NO_PROXY);
5573 +        } else {
5574 +            int pp = StringUtil.isBlank(port) ? 80 : Integer.valueOf(port);
5575 +            log.info("Using proxy", "host", host, "port", pp, "haveCreds", haveCreds);
5576 +            app.conn = new Connector(new Proxy(Proxy.Type.HTTP, new InetSocketAddress(host, pp)));
5577 +        }
5578  
5579          if (haveCreds) {
5580              final String fuser = username;
5581 @@ -205,6 +234,61 @@ public class ProxyUtil {
5582          }
5583      }
5584  
5585 +    public static class Resolver {
5586 +        public String dnsResolve (String host) {
5587 +            try {
5588 +                return InetAddress.getByName(host).getHostAddress();
5589 +            } catch (UnknownHostException uhe) {
5590 +                return null;
5591 +            }
5592 +        }
5593 +        public String myIpAddress () {
5594 +            try {
5595 +                return InetAddress.getLocalHost().getHostAddress();
5596 +            } catch (UnknownHostException uhe) {
5597 +                return null;
5598 +            }
5599 +        }
5600 +    }
5601 +
5602 +    public static String[] findPACProxiesForURL (Reader pac, URL url) {
5603 +        ScriptEngineManager manager = new ScriptEngineManager();
5604 +        ScriptEngine engine = manager.getEngineByName("javascript");
5605 +        Bindings globals = engine.createBindings();
5606 +        globals.put("resolver", new Resolver());
5607 +        engine.setBindings(globals, ScriptContext.GLOBAL_SCOPE);
5608 +        try {
5609 +            URL utils = ProxyUtil.class.getResource("PacUtils.js");
5610 +            if (utils == null) {
5611 +                log.error("Unable to load PacUtils.js");
5612 +                return new String[0];
5613 +            }
5614 +            engine.eval(new InputStreamReader(utils.openStream()));
5615 +            Object res = engine.eval(pac);
5616 +            if (engine instanceof Invocable) {
5617 +                Object[] args = new Object[] { url.toString(), url.getHost() };
5618 +                res = ((Invocable) engine).invokeFunction("FindProxyForURL", args);
5619 +            }
5620 +            String[] proxies = res.toString().split(";");
5621 +            for (int ii = 0; ii < proxies.length; ii += 1) {
5622 +                proxies[ii] = proxies[ii].trim();
5623 +            }
5624 +            return proxies;
5625 +        } catch (Exception e) {
5626 +            log.warning("Failed to resolve PAC proxy", e);
5627 +        }
5628 +        return new String[0];
5629 +    }
5630 +
5631 +    private static String[] splitHostPort (String hostPort) {
5632 +        int cidx = hostPort.indexOf(":");
5633 +        if (cidx == -1) {
5634 +            return new String[] { hostPort, null};
5635 +        } else {
5636 +            return new String[] { hostPort.substring(0, cidx), hostPort.substring(cidx+1) };
5637 +        }
5638 +    }
5639 +
5640      protected static final String PROXY_REGISTRY =
5641          "Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings";
5642  }
5643 diff --git a/getdown/src/getdown/launcher/src/main/java/com/threerings/getdown/launcher/RotatingBackgrounds.java b/getdown/src/getdown/launcher/src/main/java/com/threerings/getdown/launcher/RotatingBackgrounds.java
5644 index d3aa2bd25..d64e5f02d 100644
5645 --- a/getdown/src/getdown/launcher/src/main/java/com/threerings/getdown/launcher/RotatingBackgrounds.java
5646 +++ b/getdown/src/getdown/launcher/src/main/java/com/threerings/getdown/launcher/RotatingBackgrounds.java
5647 @@ -14,7 +14,7 @@ public final class RotatingBackgrounds
5648  {
5649      public interface ImageLoader {
5650          /** Loads and returns the image with the supplied path. */
5651 -        public Image loadImage (String path);
5652 +        Image loadImage (String path);
5653      }
5654  
5655      /**
5656 @@ -35,7 +35,7 @@ public final class RotatingBackgrounds
5657      }
5658  
5659      /**
5660 -     * Create a sequence of images to be rotated through from <code>backgrounds</code>.
5661 +     * Create a sequence of images to be rotated through from {@code backgrounds}.
5662       *
5663       * Each String in backgrounds should be the path to the image, a semicolon, and the minimum
5664       * amount of time to display the image in seconds. Each image will be active for an equal
5665 diff --git a/getdown/src/getdown/launcher/src/main/java/com/threerings/getdown/launcher/StatusPanel.java b/getdown/src/getdown/launcher/src/main/java/com/threerings/getdown/launcher/StatusPanel.java
5666 index 99f44ca51..197dc9170 100644
5667 --- a/getdown/src/getdown/launcher/src/main/java/com/threerings/getdown/launcher/StatusPanel.java
5668 +++ b/getdown/src/getdown/launcher/src/main/java/com/threerings/getdown/launcher/StatusPanel.java
5669 @@ -26,12 +26,9 @@ import com.samskivert.swing.Label;
5670  import com.samskivert.swing.LabelStyleConstants;
5671  import com.samskivert.swing.util.SwingUtil;
5672  import com.samskivert.util.Throttle;
5673 -
5674  import com.threerings.getdown.data.Application.UpdateInterface;
5675  import com.threerings.getdown.util.MessageUtil;
5676  import com.threerings.getdown.util.Rectangle;
5677 -import com.threerings.getdown.util.StringUtil;
5678 -
5679  import static com.threerings.getdown.Log.log;
5680  
5681  /**
5682 @@ -344,7 +341,7 @@ public final class StatusPanel extends JComponent
5683      {
5684          String msg = get(key);
5685          if (msg != null) return MessageFormat.format(MessageUtil.escape(msg), (Object[])args);
5686 -        return key + String.valueOf(Arrays.asList(args));
5687 +        return key + Arrays.asList(args);
5688      }
5689  
5690      /** Used by {@link #setStatus}, and {@link #setProgress}. */
5691 diff --git a/getdown/src/getdown/launcher/src/main/java/jalview/bin/StartupNotificationListener.java b/getdown/src/getdown/launcher/src/main/java/jalview/bin/StartupNotificationListener.java
5692 deleted file mode 100644
5693 index 5c6c7c393..000000000
5694 --- a/getdown/src/getdown/launcher/src/main/java/jalview/bin/StartupNotificationListener.java
5695 +++ /dev/null
5696 @@ -1,29 +0,0 @@
5697 -package jalview.bin;
5698 -
5699 -import com.threerings.getdown.launcher.GetdownApp;
5700 -import static com.threerings.getdown.Log.log;
5701 -
5702 -public class StartupNotificationListener {
5703 -
5704 -  public static void setListener() {
5705 -
5706 -    
5707 -    try {
5708 -      com.install4j.api.launcher.StartupNotification.registerStartupListener(
5709 -        new com.install4j.api.launcher.StartupNotification.Listener() {
5710 -          @Override
5711 -          public void startupPerformed(String parameters) { 
5712 -            log.info("StartupNotification.Listener.startupPerformed: '"+parameters+"'");
5713 -            GetdownApp.setStartupFilesParameterString(parameters);
5714 -          }
5715 -        }
5716 -      );
5717 -    } catch (Exception e) {
5718 -      e.printStackTrace();
5719 -    } catch (NoClassDefFoundError t) {
5720 -      log.warning("Starting without install4j classes");
5721 -    }
5722 -
5723 -  }
5724 -
5725 -}
5726 diff --git a/getdown/src/getdown/launcher/src/main/resources/com/threerings/getdown/messages.properties b/getdown/src/getdown/launcher/src/main/resources/com/threerings/getdown/messages.properties
5727 index 19b2999e1..7a33ca0e4 100644
5728 --- a/getdown/src/getdown/launcher/src/main/resources/com/threerings/getdown/messages.properties
5729 +++ b/getdown/src/getdown/launcher/src/main/resources/com/threerings/getdown/messages.properties
5730 @@ -12,11 +12,14 @@ m.abort_cancel = Continue installation
5731  m.detecting_proxy = Trying to auto-detect proxy settings
5732  
5733  m.configure_proxy = <html>We were unable to connect to the application server to download data. \
5734 -  <p> Please make sure that no virus scanner or firewall is blocking network communicaton with \
5735 +  <p> Please make sure that no virus scanner or firewall is blocking network communication with \
5736    the server. \
5737    <p> Your computer may access the Internet through a proxy and we were unable to automatically \
5738    detect your proxy settings. If you know your proxy settings, you can enter them below.</html>
5739  
5740 +m.update_proxy_auth = <html>The stored proxy user/password is wrong or obsolete. \
5741 +  <p>Please provide an updated user/password combination.</html>
5742 +
5743  m.proxy_extra = <html>If you are sure that you don't use a proxy then \
5744    perhaps there is a temporary Internet outage that is preventing us from \
5745    communicating with the servers. In this case, you can cancel and try \
5746 @@ -41,10 +44,7 @@ m.checking = Checking for update
5747  m.validating = Validating
5748  m.patching = Patching
5749  m.launching = Launching
5750 -
5751  m.patch_notes = Patch Notes
5752 -m.play_again = Play Again
5753 -
5754  m.complete = {0}% complete
5755  m.remain = {0} remaining
5756  
5757 @@ -99,7 +99,7 @@ m.default_install_error = the support section of the website
5758  m.another_getdown_running = Multiple instances of this application's \
5759   installer are running. This one will stop and let another complete.
5760  
5761 -m.applet_stopped = Getdown's applet was told to stop working.
5762 +m.verify_timeout = Verifying resources took too long.
5763  
5764  # application/digest errors
5765  m.missing_appbase = The configuration file is missing the 'appbase'.
5766 diff --git a/getdown/src/getdown/launcher/src/main/resources/com/threerings/getdown/messages_de.properties b/getdown/src/getdown/launcher/src/main/resources/com/threerings/getdown/messages_de.properties
5767 index 8e3683594..db35593b2 100644
5768 --- a/getdown/src/getdown/launcher/src/main/resources/com/threerings/getdown/messages_de.properties
5769 +++ b/getdown/src/getdown/launcher/src/main/resources/com/threerings/getdown/messages_de.properties
5770 @@ -1,13 +1,9 @@
5771  #
5772 -# $Id$
5773 -#
5774 -# Getdown translation messages
5775 +# Getdown German translation messages
5776  
5777  m.abort_title = Installation abbrechen?
5778 -m.abort_confirm = <html>Bist du sicher, dass du die Installation abbrechen \
5779 -m\u00f6chtest? \
5780 -  Du kannst sp\u00e4ter fortfahren, indem du die Anwendung erneut \
5781 -ausf\u00fchrst.</html>
5782 +m.abort_confirm = <html>Bist du sicher, dass du die Installation abbrechen möchtest? \
5783 +  Du kannst später fortfahren, indem du die Anwendung erneut ausführst.</html>
5784  m.abort_ok = Beenden
5785  m.abort_cancel = Installation fortsetzen
5786  
5787 @@ -17,9 +13,12 @@ m.configure_proxy = <html>Es konnte keine Verbindung zum Applikations-Server auf
5788    <p>Bitte kontrollieren Sie die Proxyeinstellungen und stellen Sie sicher, dass keine lokal oder \
5789    im Netzwerk betriebene Sicherheitsanwendung (Virenscanner, Firewall, etc.) die Kommunikation \
5790    mit dem Server blockiert.<br> \
5791 -  Wenn kein Proxy verwendet werden soll, l\u00f6schen Sie bitte alle Eintr\u00e4ge in den unten \
5792 +  Wenn kein Proxy verwendet werden soll, löschen Sie bitte alle Einträge in den unten \
5793    stehenden Feldern und klicken sie auf OK.</html>
5794  
5795 +m.update_proxy_auth = <html>Gespeicherte Proxy User/Passwort Kombination ungültig oder obsolet. \
5796 +  <p>Bitte geben Sie die korrekte User/Passwort Kombination ein.</html>
5797 +
5798  m.proxy_extra = <html>Sollten Sie keine Proxyeinstellungen gesetzt haben wenden Sie sich bitte \
5799    an Ihren Administrator.</html>
5800  
5801 @@ -46,71 +45,68 @@ m.launching = Starte
5802  m.patch_notes = Patchnotes
5803  
5804  m.complete = {0}% abgeschlossen
5805 -m.remain = {0} \u00fcbrig
5806 +m.remain = {0} übrig
5807  
5808  m.updating_metadata = Lade Steuerungsdateien herunter
5809  
5810 -m.init_failed = Unsere Konfigurationsdatei fehlt oder ist besch\u00e4digt. \
5811 -Versuche, eine neue Kopie herunterzuladen...
5812 +m.init_failed = Unsere Konfigurationsdatei fehlt oder ist beschädigt. \
5813 +  Versuche, eine neue Kopie herunterzuladen...
5814  
5815 -m.java_download_failed = Wir konnten die notwendige Javaversion f\u00fcr deinen \
5816 -Computer nicht automatisch herunterladen. \n\n \
5817 -Bitte auf www.java.com die aktuelle Javaversion herunterladen und dann die \
5818 -Anwendung erneut starten.
5819 +m.java_download_failed = Wir konnten die notwendige Javaversion für deinen \
5820 +  Computer nicht automatisch herunterladen. \n\n \
5821 +  Bitte auf www.java.com die aktuelle Javaversion herunterladen und dann die \
5822 +  Anwendung erneut starten.
5823  
5824  m.java_unpack_failed = Wir konnten die aktualisierte Javaversion nicht \
5825 -entpacken. Bitte stelle sicher, dass wenigstens 100MB Platz auf der \
5826 -Festplatte frei sind und versuche dann die Anwendung erneut zu \
5827 -starten.\n\n\ \
5828 -Falls das das Problem nicht beseitigt, bitte auf www.java.com die aktuelle \
5829 -Javaversion herunterladen und installieren und dann erneut versuchen.
5830 +  entpacken. Bitte stelle sicher, dass wenigstens 100MB Platz auf der \
5831 +  Festplatte frei sind und versuche dann die Anwendung erneut zu \
5832 +  starten.\n\n\ \
5833 +  Falls das das Problem nicht beseitigt, bitte auf www.java.com die aktuelle \
5834 +  Javaversion herunterladen und installieren und dann erneut versuchen.
5835  
5836  m.unable_to_repair = Wir konnten die notwendigen Dateien nach 5 Versuchen \
5837 -nicht herunterladen. Du kannst versuchen, die Anwendung erneut zu starten, \
5838 -aber wenn dies erneut fehlschl\u00e4gt, musst du die Anwendung deinstallieren \
5839 -und erneut installieren.
5840 +  nicht herunterladen. Du kannst versuchen, die Anwendung erneut zu starten, \
5841 +  aber wenn dies erneut fehlschlägt, musst du die Anwendung deinstallieren \
5842 +  und erneut installieren.
5843  
5844  m.unknown_error = Die Anwendung konnte wegen eines unbekannten Fehlers \
5845 -nicht gestartet werden. Bitte auf \n{0} weiterlesen.
5846 +  nicht gestartet werden. Bitte auf \n{0} weiterlesen.
5847  
5848  m.init_error = Die Anwendung konnte wegen folgendem Fehler nicht gestartet \
5849 -werden:\n{0}\n\n Bitte auf \n{1} weiterlesen, um zu erfahren, wie bei \
5850 -solchen Problemen vorzugehen ist.
5851 +  werden:\n{0}\n\n Bitte auf \n{1} weiterlesen, um zu erfahren, wie bei \
5852 +  solchen Problemen vorzugehen ist.
5853  
5854 -m.readonly_error = Das Verzeichnis, in dem die Anwendung installiert ist: \
5855 - \n{0}\nist nicht schreibberechtigt. Bitte in ein Verzeichnis mit \
5856 -Schreibzugriff installieren.
5857 +m.readonly_error = Das Verzeichnis, in dem die Anwendung installiert ist:\n{0}\n \
5858 +  ist nicht schreibberechtigt. Bitte in ein Verzeichnis mit Schreibzugriff installieren.
5859  
5860  m.missing_resource = Die Anwendung konnte nicht gestartet werden, da die \
5861 -folgende Quelle nicht gefunden wurde:\n{0}\n\n\ Bitte auf \n{1} \
5862 -weiterlesen, um zu erfahren, wie bei solchen Problemen vorzugehen ist.
5863 +  folgende Quelle nicht gefunden wurde:\n{0}\n\n\ Bitte auf \n{1} \
5864 +  weiterlesen, um zu erfahren, wie bei solchen Problemen vorzugehen ist.
5865  
5866  m.insufficient_permissions_error = Du hast die digitale Signatur dieser \
5867 -Anwendung nicht akzeptiert. Falls du diese Anwendung benutzen willst, \
5868 -musst du ihre digitale Signatur akzeptieren. \n\Um das zu tun, musst du \
5869 -deinen Browser beenden, neu starten und erneut die Anwendung von dieser \
5870 -Webseite aus starten. Wenn die Sicherheitsabfrage erscheint, bitte die \
5871 -digitale Signatur akzeptieren, um der Anwendung die n\u00f6tigen Rechte zu \
5872 -geben, die sie braucht, um zu laufen.
5873 +  Anwendung nicht akzeptiert. Falls du diese Anwendung benutzen willst, \
5874 +  musst du ihre digitale Signatur akzeptieren. \n\Um das zu tun, musst du \
5875 +  deinen Browser beenden, neu starten und erneut die Anwendung von dieser \
5876 +  Webseite aus starten. Wenn die Sicherheitsabfrage erscheint, bitte die \
5877 +  digitale Signatur akzeptieren, um der Anwendung die nötigen Rechte zu \
5878 +  geben, die sie braucht, um zu laufen.
5879  
5880  m.corrupt_digest_signature_error = Wir konnten die digitale Signatur \
5881 -dieser Anwendung nicht \u00fcberpr\u00fcfen.\nBitte \u00fcberpr\u00fcfe, ob du die Anwendung \
5882 -von der richtigen Webseite aus startest.
5883 +  dieser Anwendung nicht überprüfen.\nBitte überprüfe, ob du die Anwendung \
5884 +  von der richtigen Webseite aus startest.
5885  
5886  m.default_install_error = der Support-Webseite
5887  
5888 -m.another_getdown_running = Diese Installationsanwendung l\u00e4uft in mehreren \
5889 -Instanzen. Diese Instanz wird sich beenden und eine andere Instanz den \
5890 -Vorgang erledigen lassen.
5891 -
5892 -m.applet_stopped = Die Anwendung wurde beendet.
5893 +m.another_getdown_running = Diese Installationsanwendung läuft in mehreren \
5894 +  Instanzen. Diese Instanz wird sich beenden und eine andere Instanz den \
5895 +  Vorgang erledigen lassen.
5896  
5897 +m.verify_timeout = Timeout beim Verifizieren der Resourcen.
5898  
5899  # application/digest errors
5900  m.missing_appbase = In der Konfigurationsdatei fehlt die 'appbase'.
5901  m.invalid_version = In der Konfigurationsdatei steht die falsche Version.
5902  m.invalid_appbase = In der Konfigurationsdatei steht die falsche 'appbase'.
5903  m.missing_class = In der Konfigurationsdatei fehlt die Anwendungsklasse.
5904 -m.missing_code = Die Konfigurationsdatei enth\u00e4lt keine Codequellen.
5905 -m.invalid_digest_file = Die Hashwertedatei ist ung\u00fcltig.
5906 -
5907 +m.missing_code = Die Konfigurationsdatei enthält keine Codequellen.
5908 +m.invalid_digest_file = Die Hashwertedatei ist ungültig.
5909 diff --git a/getdown/src/getdown/launcher/src/main/resources/com/threerings/getdown/messages_es.properties b/getdown/src/getdown/launcher/src/main/resources/com/threerings/getdown/messages_es.properties
5910 index 609b02524..46cd64ac9 100644
5911 --- a/getdown/src/getdown/launcher/src/main/resources/com/threerings/getdown/messages_es.properties
5912 +++ b/getdown/src/getdown/launcher/src/main/resources/com/threerings/getdown/messages_es.properties
5913 @@ -1,36 +1,34 @@
5914  #
5915 -# $Id$
5916 -#
5917 -# Getdown translation messages
5918 +# Getdown Spanish translation messages
5919  
5920 -m.abort_title = \u00bfCancelar la instalaci\u00f3n?
5921 -m.abort_confirm = <html>\u00bfEst\u00e1s seguro de querer cancelar la instalaci\u00f3n? \
5922 -  Puedes continuarla despu\u00e9s si corres de nuevo la aplicaci\u00f3n.</html>
5923 +m.abort_title = ¿Cancelar la instalación?
5924 +m.abort_confirm = <html>¿Estás seguro de querer cancelar la instalación? \
5925 +  Puedes continuarla después si corres de nuevo la aplicación.</html>
5926  m.abort_ok = Cancelar
5927 -m.abort_cancel = Continuar la instalaci\u00f3n
5928 +m.abort_cancel = Continuar la instalación
5929  
5930 -m.detecting_proxy = Detectando autom\u00e1ticamente la configuraci\u00f3n proxy
5931 +m.detecting_proxy = Detectando automáticamente la configuración proxy
5932  
5933  m.configure_proxy = <html>No ha sido posible conectar con nuestros servidores para \
5934    descargar los datos del juego. \
5935    <ul><li> Si el cortafuegos de Windows o Norton Internet Security tiene instrucciones \
5936 -  de bloquear <code>javaw.exe</code> no podemos descargar el juego. Necesitar\u00e1s \
5937 +  de bloquear <code>javaw.exe</code> no podemos descargar el juego. Necesitarás \
5938    permitir que <code>javaw.exe</code> tenga acceso al Internet. Puedes intentar \
5939    correr el juego de nuevo, pero es posible que debas dar permisos a javaw.exe en la \
5940 -  configuraci\u00f3n de tu cortafuegos ( Inicio -> Panel de control -> Firewall de Windows ).</ul> \
5941 +  configuración de tu cortafuegos ( Inicio -> Panel de control -> Firewall de Windows ).</ul> \
5942    <p> Es posible que tu computadora tenga acceso al Internet por medio de un proxy por lo que \
5943 -  no ha sido posible detectar autom\u00e1ticamente tu configuraci\u00f3n. Si conoces tu \
5944 -  configuraci\u00f3n proxy, puedes anotarla abajo.</html>
5945 +  no ha sido posible detectar automáticamente tu configuración. Si conoces tu \
5946 +  configuración proxy, puedes anotarla abajo.</html>
5947  
5948 -m.proxy_extra = <html>Si est\u00e1s seguro de que no tienes un proxy entonces \
5949 -  tal vez exista un falla temporal en el Internet que est\u00e1 evitando que podamos \
5950 +m.proxy_extra = <html>Si estás seguro de que no tienes un proxy entonces \
5951 +  tal vez exista un falla temporal en el Internet que está evitando que podamos \
5952    comunicarnos con los servidores. En este caso, puedes cancelar e intentar \
5953 -  instalarla de nuevo m\u00e1s tarde.</html>
5954 +  instalarla de nuevo más tarde.</html>
5955  
5956  m.proxy_host = IP proxy
5957  m.proxy_port = Puerto proxy
5958  m.proxy_username = Nombre de usuario
5959 -m.proxy_password = Contrase\u00f1a
5960 +m.proxy_password = Contraseña
5961  m.proxy_auth_required = Autenticacion requerida
5962  m.proxy_ok = OK
5963  m.proxy_cancel = Cancelar
5964 @@ -54,62 +52,59 @@ m.remain = {0} restante
5965  
5966  m.updating_metadata = Descargando los archivos de control
5967  
5968 -m.init_failed = Un archivo de configuraci\u00f3n est\u00e1 faltante o est\u00e1 corrupto. Intentando \
5969 +m.init_failed = Un archivo de configuración está faltante o está corrupto. Intentando \
5970    descargar una nueva copia...
5971  
5972 -m.java_download_failed = No ha sido posible descargar autom\u00e1ticamente la \
5973 -  versi\u00f3n de Java necesaria para tu computadora.\n\n\
5974 -  Por favor ve a www.java.com y descarga la \u00faltima versi\u00f3n de \
5975 -  Java, despu\u00e9s intenta correr de nuevo la aplicaci\u00f3n.
5976 +m.java_download_failed = No ha sido posible descargar automáticamente la \
5977 +  versión de Java necesaria para tu computadora.\n\n\
5978 +  Por favor ve a www.java.com y descarga la última versión de \
5979 +  Java, después intenta correr de nuevo la aplicación.
5980  
5981 -m.java_unpack_failed = No ha sido posible desempacar una versi\u00f3n actualizada de \
5982 -  Java. Por favor aseg\u00farate de tener al menos 100 MB de espacio libre en tu \
5983 -  disco duro e intenta correr de nuevo la aplicaci\u00f3n.\n\n\
5984 +m.java_unpack_failed = No ha sido posible desempacar una versión actualizada de \
5985 +  Java. Por favor asegúrate de tener al menos 100 MB de espacio libre en tu \
5986 +  disco duro e intenta correr de nuevo la aplicación.\n\n\
5987    Si eso no soluciona el problema, ve a www.java.com y descarga e \
5988 -  instala la \u00faltima versi\u00f3n de Java e intenta de nuevo.
5989 +  instala la última versión de Java e intenta de nuevo.
5990  
5991 -m.unable_to_repair = No ha sido posible descargar los archivos necesarios despu\u00e9s de \
5992 -  cinco intentos. Puedes intentar correr de nuevo la aplicaci\u00f3n, pero si falla \
5993 -  de nuevo podr\u00edas necesitar desinstalar y reinstalar.
5994 +m.unable_to_repair = No ha sido posible descargar los archivos necesarios después de \
5995 +  cinco intentos. Puedes intentar correr de nuevo la aplicación, pero si falla \
5996 +  de nuevo podrías necesitar desinstalar y reinstalar.
5997  
5998 -m.unknown_error = La aplicaci\u00f3n no ha podido iniciar debido a un extra\u00f1o \
5999 -  error del que no se pudo recobrar. Por favor visita\n{0} para ver informaci\u00f3n acerca \
6000 +m.unknown_error = La aplicación no ha podido iniciar debido a un extraño \
6001 +  error del que no se pudo recobrar. Por favor visita\n{0} para ver información acerca \
6002    de como recuperarla.
6003 -m.init_error = La aplicaci\u00f3n no ha podido iniciar debido al siguiente \
6004 +m.init_error = La aplicación no ha podido iniciar debido al siguiente \
6005    error:\n{0}\n\nPor favor visita\n{1} para \
6006 -  ver informaci\u00f3n acerca de como manejar ese tipo de problemas.
6007 +  ver información acerca de como manejar ese tipo de problemas.
6008  
6009 -m.readonly_error = El directorio en el que esta aplicaci\u00f3n est\u00e1 instalada: \
6010 -  \n{0}\nes solo lectura. Por favor instala la aplicaci\u00f3n en un directorio en el cual \
6011 +m.readonly_error = El directorio en el que esta aplicación está instalada: \
6012 +  \n{0}\nes solo lectura. Por favor instala la aplicación en un directorio en el cual \
6013    tengas acceso de escritura.
6014  
6015 -m.missing_resource = La aplicaci\u00f3n no ha podido iniciar debido a un recurso \
6016 -  faltante:\n{0}\n\nPor favor visita\n{1} para informaci\u00f3n acerca de como solucionar \
6017 +m.missing_resource = La aplicación no ha podido iniciar debido a un recurso \
6018 +  faltante:\n{0}\n\nPor favor visita\n{1} para información acerca de como solucionar \
6019    estos problemas.
6020  
6021  m.insufficient_permissions_error = No aceptaste la firma digital de \
6022 - esta aplicaci\u00f3n. Si quieres correr la aplicaci\u00f3n, necesitas aceptar \
6023 + esta aplicación. Si quieres correr la aplicación, necesitas aceptar \
6024   su firma digital.\n\nPara hacerlo, necesitas cerrar tu navegador, \
6025 - reiniciarlo, y regresar a esta p\u00e1gina web para reiniciar la aplicaci\u00f3n. Cuando se muestre \
6026 - el di\u00e1logo de seguridad, haz clic en el bot\u00f3n para aceptar la firmar digital \
6027 - y otorgar a esta aplicaci\u00f3n los privilegios que necesita para correr.
6028 + reiniciarlo, y regresar a esta página web para reiniciar la aplicación. Cuando se muestre \
6029 + el diálogo de seguridad, haz clic en el botón para aceptar la firmar digital \
6030 + y otorgar a esta aplicación los privilegios que necesita para correr.
6031  
6032  m.corrupt_digest_signature_error = No pudimos verificar la firma digital \
6033 - de la aplicaci\u00f3n.\nPor favor revisa que est\u00e9s lanzando la aplicaci\u00f3n desde\nel \
6034 + de la aplicación.\nPor favor revisa que estés lanzando la aplicación desde\nel \
6035   sitio web correcto.
6036  
6037 -m.default_install_error = la secci\u00f3n de asistencia de este sitio web
6038 -
6039 -m.another_getdown_running = Est\u00e1n corriendo m\u00faltiples instancias de \
6040 - este instalador.  Este se detendr\u00e1 para permitir que otra contin\u00fae.
6041 +m.default_install_error = la sección de asistencia de este sitio web
6042  
6043 -m.applet_stopped = Se le dijo al applet de Getdown que dejara de trabajar.
6044 +m.another_getdown_running = Están corriendo múltiples instancias de \
6045 + este instalador.  Este se detendrá para permitir que otra continúe.
6046  
6047  # application/digest errors
6048 -m.missing_appbase = Al archivo de configuraci\u00f3n le falta el 'appbase'.
6049 -m.invalid_version = El archivo de configuraci\u00f3n especifica una versi\u00f3n no v\u00e1lida.
6050 -m.invalid_appbase = El archivo de configuraci\u00f3n especifica un 'appbase' no v\u00e1lido.
6051 -m.missing_class = Al archivo de configuraci\u00f3n le falta la clase de aplicaci\u00f3n.
6052 -m.missing_code = El archivo de configuraci\u00f3n especifica que no hay recursos de c\u00f3digo.
6053 -m.invalid_digest_file = El archivo digest no es v\u00e1lido.
6054 -
6055 +m.missing_appbase = Al archivo de configuración le falta el 'appbase'.
6056 +m.invalid_version = El archivo de configuración especifica una versión no válida.
6057 +m.invalid_appbase = El archivo de configuración especifica un 'appbase' no válido.
6058 +m.missing_class = Al archivo de configuración le falta la clase de aplicación.
6059 +m.missing_code = El archivo de configuración especifica que no hay recursos de código.
6060 +m.invalid_digest_file = El archivo digest no es válido.
6061 diff --git a/getdown/src/getdown/launcher/src/main/resources/com/threerings/getdown/messages_fr.properties b/getdown/src/getdown/launcher/src/main/resources/com/threerings/getdown/messages_fr.properties
6062 index 3666204e2..5eb8ec9cd 100644
6063 --- a/getdown/src/getdown/launcher/src/main/resources/com/threerings/getdown/messages_fr.properties
6064 +++ b/getdown/src/getdown/launcher/src/main/resources/com/threerings/getdown/messages_fr.properties
6065 @@ -1,29 +1,27 @@
6066  #
6067 -# $Id: messages.properties 485 2012-03-08 22:05:30Z ray.j.greenwell $
6068 -#
6069 -# Getdown translation messages
6070 +# Getdown French translation messages
6071  
6072  m.abort_title = Annuler l'installation?
6073 -m.abort_confirm =<html>\u00cates-vous s\u00fbr de vouloir annuler l'installation? \
6074 -   Vous pourrez reprendre l'installation en ex\u00e9cutant l'application de nouveau.</html>
6075 +m.abort_confirm =<html>Êtes-vous sûr de vouloir annuler l'installation? \
6076 +   Vous pourrez reprendre l'installation en exécutant l'application de nouveau.</html>
6077  m.abort_ok = Quitter
6078  m.abort_cancel = Continuer l'installation
6079  
6080 -m.detecting_proxy = D\u00e9tection automatique des r\u00e9glages proxy
6081 +m.detecting_proxy = Détection automatique des réglages proxy
6082  
6083  m.configure_proxy =<html>Connexion au serveur impossible. \
6084 -   <ul><li>  Veuillez v\u00e9rifier que <code>javaw.exe</code> n'est bloqu\u00e9 \
6085 +   <ul><li>  Veuillez vérifier que <code>javaw.exe</code> n'est bloqué \
6086     par aucun pare-feu ou antivirus. \
6087     Vous pouvez vous rendre sur la configuration du pare-feu windows via \
6088 -   (D\u00e9marrer ->  Panneau de Configuration ->  Pare-feu Windows ).</ul> \
6089 -   <p>  Il est \u00e9galement possible que vous soyez derri\u00e8re un proxy que l'application \
6090 -   est incapable de d\u00e9tecter automatiquement. \
6091 -   Si tel est le cas, veuillez saisir les r\u00e9glages proxy ci-dessous.</html>
6092 +   (Démarrer ->  Panneau de Configuration ->  Pare-feu Windows ).</ul> \
6093 +   <p>  Il est également possible que vous soyez derrière un proxy que l'application \
6094 +   est incapable de détecter automatiquement. \
6095 +   Si tel est le cas, veuillez saisir les réglages proxy ci-dessous.</html>
6096  
6097 -m.proxy_extra =<html>Si vous \u00eates certain de ne pas utiliser de proxy, il est \
6098 -   possible qu'une interruption temporaire de la connexion internet emp\u00fbche la \
6099 +m.proxy_extra =<html>Si vous êtes certain de ne pas utiliser de proxy, il est \
6100 +   possible qu'une interruption temporaire de la connexion internet empûche la \
6101     communication avec les serveurs. Dans ce cas, vous pouvez relancer \
6102 -   l'installation ult\u00e9rieurement.</html>
6103 +   l'installation ultérieurement.</html>
6104  
6105  m.proxy_host = Proxy IP
6106  m.proxy_port = Proxy port
6107 @@ -33,79 +31,77 @@ m.proxy_auth_required = Identification requise
6108  m.proxy_ok = OK
6109  m.proxy_cancel = Annuler
6110  
6111 -m.downloading_java = T\u00e9l\u00e9chargement en cours de la Machine Virtuelle Java
6112 -m.unpacking_java = D\u00e9compression en cours de la Machine Virtuelle Java
6113 +m.downloading_java = Téléchargement en cours de la Machine Virtuelle Java
6114 +m.unpacking_java = Décompression en cours de la Machine Virtuelle Java
6115  
6116 -m.resolving = R\u00e9solution des t\u00e9l\u00e9chargements en cours
6117 -m.downloading = T\u00e9l\u00e9chargement des donn\u00e9es en cours
6118 -m.failure = \u00c9chec du t\u00e9l\u00e9chargement: {0}
6119 +m.resolving = Résolution des téléchargements en cours
6120 +m.downloading = Téléchargement des données en cours
6121 +m.failure = Échec du téléchargement: {0}
6122  
6123 -m.checking = V\u00e9rification de la mise-\u00e0-jour en cours
6124 +m.checking = Vérification de la mise-à-jour en cours
6125  m.validating = Validation en cours
6126  m.patching = Modification en cours
6127  m.launching = Lancement en cours
6128  
6129 -m.patch_notes = Notes de mise-\u00e0-jour
6130 +m.patch_notes = Notes de mise-à-jour
6131  
6132 -m.complete = Complet \u00e0 {0}%
6133 +m.complete = Complet à {0}%
6134  m.remain = {0} restant
6135  
6136 -m.updating_metadata = T\u00e9l\u00e9chargement des fichiers de contr\u00f4les en cours
6137 +m.updating_metadata = Téléchargement des fichiers de contrôles en cours
6138  
6139 -m.init_failed = Notre fichier de configuration est perdu ou corrompu. T\u00e9l\u00e9chargement \
6140 +m.init_failed = Notre fichier de configuration est perdu ou corrompu. Téléchargement \
6141     d'une nouvelle copie en cours ...
6142  
6143 -m.java_download_failed = Impossible de t\u00e9l\u00e9charger automatiquement la \
6144 -   version de Java n\u00e9cessaire.\n\n\
6145 -   Veuillez vous rendre sur www.java.com et t\u00e9l\u00e9charger et installer la version \
6146 -   la plus r\u00e9cente de Java, avant d'ex\u00e9cuter l'application \u00e0 nouveau.
6147 +m.java_download_failed = Impossible de télécharger automatiquement la \
6148 +   version de Java nécessaire.\n\n\
6149 +   Veuillez vous rendre sur www.java.com et télécharger et installer la version \
6150 +   la plus récente de Java, avant d'exécuter l'application à nouveau.
6151  
6152 -m.java_unpack_failed = Impossible de d\u00e9compresser la version de \
6153 -   Java n\u00e9cessaire. Veuillez v\u00e9rifier que vous avez au moins 100 MB d'espace libre \
6154 -   sur votre disque dur puis tenter d'ex\u00e9cuter l'application \u00e0 nouveau.\n\n\
6155 -   Si le probl\u00e8me persiste, rendez vous www.java.com et t\u00e9l\u00e9chargez et \
6156 -   installez la version plus r\u00e9cente de Java puis essayez de nouveau.
6157 +m.java_unpack_failed = Impossible de décompresser la version de \
6158 +   Java nécessaire. Veuillez vérifier que vous avez au moins 100 MB d'espace libre \
6159 +   sur votre disque dur puis tenter d'exécuter l'application à nouveau.\n\n\
6160 +   Si le problème persiste, rendez vous www.java.com et téléchargez et \
6161 +   installez la version plus récente de Java puis essayez de nouveau.
6162  
6163 -m.unable_to_repair = Impossible de t\u00e9l\u00e9charger les fichiers n\u00e9cessaires apr\u00e8s \
6164 -   cinq tentatives. Vous pouvez tenter d'ex\u00e9cuter l'application \u00e0 nouveau, mais il est \
6165 -   possible qu'une d\u00e9sinstallation / r\u00e9installation soit n\u00e9cessaire.
6166 +m.unable_to_repair = Impossible de télécharger les fichiers nécessaires après \
6167 +   cinq tentatives. Vous pouvez tenter d'exécuter l'application à nouveau, mais il est \
6168 +   possible qu'une désinstallation / réinstallation soit nécessaire.
6169  
6170 -m.unknown_error = Une erreur inconnue a fait \u00e9chouer le lancement de l'application. \
6171 +m.unknown_error = Une erreur inconnue a fait échouer le lancement de l'application. \
6172     Veuillez visiter\n{0} pour plus d'informations.
6173 -m.init_error = Le lancement de l'application a \u00e9chou\u00e9 \u00e0 cause de l'erreur \
6174 +m.init_error = Le lancement de l'application a échoué à cause de l'erreur \
6175     suivante:\n{0}\n\nVeuillez visiter\n{1} pour plus d'informations.
6176  
6177 -m.readonly_error = Le r\u00e9pertoire d'installation de cette application: \
6178 -   \n{0}\nest en lecture seule. Veuillez installer l'application dans un r\u00e9pertoire avec \
6179 -   un acc\u00e8s en \u00e9criture.
6180 +m.readonly_error = Le répertoire d'installation de cette application: \
6181 +   \n{0}\nest en lecture seule. Veuillez installer l'application dans un répertoire avec \
6182 +   un accès en écriture.
6183  
6184 -m.missing_resource = Le lancement de l'application a \u00e9chou\u00e9 \u00e0 cause d'une \
6185 +m.missing_resource = Le lancement de l'application a échoué à cause d'une \
6186     ressource manquante:\n{0}\n\nVeuillez visiter\n{1} pour plus d'informations.
6187  
6188  m.insufficient_permissions_error = Vous n'avez pas accepter la signature \
6189 -  num\u00e9rique de cette application. Si vous souhaitez ex\u00e9cuter cette application, vous \
6190 -  devez accepter sa signature num\u00e9rique.\n\nAfin de le faire, vous devez quitter votre \
6191 -  navigateur, le red\u00e9marrer, retourner \u00e0 cette page puis relancer l'application. \
6192 -  Une fois la bo\u00eete de dialogue de s\u00e9curit\u00e9 affich\u00e9e, cliquez sur le bouton \
6193 -  pour accepter la signature num\u00e9rique et accorder les permissions n\u00e9cessaires au bon \
6194 +  numérique de cette application. Si vous souhaitez exécuter cette application, vous \
6195 +  devez accepter sa signature numérique.\n\nAfin de le faire, vous devez quitter votre \
6196 +  navigateur, le redémarrer, retourner à cette page puis relancer l'application. \
6197 +  Une fois la boîte de dialogue de sécurité affichée, cliquez sur le bouton \
6198 +  pour accepter la signature numérique et accorder les permissions nécessaires au bon \
6199    fonctionnement de l'application.
6200  
6201 -m.corrupt_digest_signature_error = Nous ne pouvons pas v\u00e9rifier la signature num\u00e9rique \
6202 -  de l'application.\nVeuillez v\u00e9rifier que vous lancez l'application \ndepuis \
6203 +m.corrupt_digest_signature_error = Nous ne pouvons pas vérifier la signature numérique \
6204 +  de l'application.\nVeuillez vérifier que vous lancez l'application \ndepuis \
6205    la bonne adresse internet.
6206  
6207  m.default_install_error = la section de support du site
6208  
6209  m.another_getdown_running = Plusieurs instances d'installation de cette \
6210 -  application sont d\u00e9j\u00e0 en cours d'ex\u00e9cution.  Cette instance va s'arr\u00eater \
6211 +  application sont déjà en cours d'exécution.  Cette instance va s'arrêter \
6212    afin de permettre aux autres d'aboutir.
6213  
6214 -m.applet_stopped = L'appelet Getdown a \u00e9t\u00e9 stopp\u00e9e.
6215 -
6216  # application/digest errors
6217  m.missing_appbase = Le fichier de configuration ne contient pas 'appbase'.
6218 -m.invalid_version = Le fichier de configuration sp\u00e9cifie une version invalide.
6219 -m.invalid_appbase = Le fichier de configuration sp\u00e9cifie un 'appbase' invalide.
6220 +m.invalid_version = Le fichier de configuration spécifie une version invalide.
6221 +m.invalid_appbase = Le fichier de configuration spécifie un 'appbase' invalide.
6222  m.missing_class = Le fichier de configuration ne contient pas la classe de l'application.
6223 -m.missing_code = Le fichier de configuration ne sp\u00e9cifie aucune ressource de code.
6224 +m.missing_code = Le fichier de configuration ne spécifie aucune ressource de code.
6225  m.invalid_digest_file = Le fichier digest est invalide.
6226 diff --git a/getdown/src/getdown/launcher/src/main/resources/com/threerings/getdown/messages_it.properties b/getdown/src/getdown/launcher/src/main/resources/com/threerings/getdown/messages_it.properties
6227 index 33b3260ce..aea9e9017 100644
6228 --- a/getdown/src/getdown/launcher/src/main/resources/com/threerings/getdown/messages_it.properties
6229 +++ b/getdown/src/getdown/launcher/src/main/resources/com/threerings/getdown/messages_it.properties
6230 @@ -1,7 +1,5 @@
6231  #
6232 -# $Id$
6233 -#
6234 -# Getdown translation messages
6235 +# Getdown Italian translation messages
6236  
6237  m.abort_title = Annullare l'installazione?
6238  m.abort_confirm = <html>Sei sicuro di voler annullare l'installazione? \
6239 @@ -22,7 +20,11 @@ m.configure_proxy = <html>Impossibile collegarsi al server per \
6240    questo potrebbe non essere stato riconosciuto automaticamente. Se conosci le \
6241    tue impostazioni del proxy, puoi inserirle di seguito.</html>
6242  
6243 -m.proxy_extra = <html>Se sei sicuro di non usare proxy  \
6244 +m.update_proxy_auth = <html>Combinazione User/Password salvata per il proxy non è valida \
6245 +  oppure obsoleta. \
6246 +  <p>Perfavore inserire una combinazione User/Password valida.</html>
6247 +
6248 +m.proxy_extra = <html>Se sei sicuro di non usare proxy \
6249    potrebbe essere un problema di internet o di collegamento con il server. \
6250    In questo caso puoi annullare e ripetere l'installazione più tardi.</html>
6251  
6252 @@ -47,8 +49,6 @@ m.patching = Applico le patch
6253  m.launching = Avvio
6254  
6255  m.patch_notes = Note delle Patch
6256 -m.play_again = Avvia Nuovamente
6257 -
6258  m.complete = {0}% completato
6259  m.remain = {0} rimasto
6260  
6261 @@ -103,8 +103,6 @@ m.default_install_error = la sezione di supporto del sito
6262  m.another_getdown_running = E' già in esecuzione un'istanza del programma. \
6263   Questa verrà chiusa.
6264  
6265 -m.applet_stopped = L'applet di Getdown è stata interrotta.
6266 -
6267  # application/digest errors
6268  m.missing_appbase = Il tag "appbase" è mancante.
6269  m.invalid_version = Il file di configurazione non contiene una versione valida (tag "version").
6270 diff --git a/getdown/src/getdown/launcher/src/main/resources/com/threerings/getdown/messages_ja.properties b/getdown/src/getdown/launcher/src/main/resources/com/threerings/getdown/messages_ja.properties
6271 index c344c16e0..f3538d0ac 100644
6272 --- a/getdown/src/getdown/launcher/src/main/resources/com/threerings/getdown/messages_ja.properties
6273 +++ b/getdown/src/getdown/launcher/src/main/resources/com/threerings/getdown/messages_ja.properties
6274 @@ -1,107 +1,105 @@
6275  #
6276 -# $Id$
6277 -#
6278 -# Getdown translation messages
6279 -
6280 -m.abort_title = \u30a4\u30f3\u30b9\u30c8\u30fc\u30eb\u3092\u4e2d\u6b62\u3057\u307e\u3059\u304b\uff1f 
6281 -m.abort_confirm = <html>\u672c\u5f53\u306b\u30a4\u30f3\u30b9\u30c8\u30fc\u30eb\u3092\u4e2d\u6b62\u3057\u307e\u3059\u304b\uff1f  \
6282 -  \u5f8c\u3067\u30a2\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3\u3092\u8d77\u52d5\u3057\u305f\u969b\u306b\u30a4\u30f3\u30b9\u30c8\u30fc\u30eb\u3092\u518d\u958b\u3067\u304d\u307e\u3059\u3002</html> 
6283 -m.abort_ok = \u4e2d\u6b62 
6284 -m.abort_cancel = \u30a4\u30f3\u30b9\u30c8\u30fc\u30eb\u306e\u7d9a\u884c 
6285 -
6286 -m.detecting_proxy = \u81ea\u52d5\u30d7\u30ed\u30ad\u30b7\u8a2d\u5b9a\u5b9f\u884c\u4e2d
6287 -
6288 -m.configure_proxy = <html>\u30b5\u30fc\u30d0\u306b\u63a5\u7d9a\u3067\u304d\u306a\u3044\u305f\u3081\u3001\u30b2\u30fc\u30e0\u306e\u30c0\u30a6\u30f3\u30ed\u30fc\u30c9\u306b \
6289 -  \u5931\u6557\u3057\u307e\u3057\u305f\u3002  \
6290 -  <ul><li>\u30a6\u30a3\u30f3\u30c9\u30a6\u30ba\u30d5\u30a1\u30a4\u30a2\u30a6\u30a9\u30fc\u30eb\u307e\u305f\u306f\u30ce\u30fc\u30c8\u30f3\u30a4\u30f3\u30bf\u30fc\u30cd\u30c3\u30c8\u30bb\u30ad\u30e5\u30ea\u30c6\u30a3\u304c \
6291 -  <code>javaw.exe</code>\u3092\u30d6\u30ed\u30c3\u30af\u3059\u308b\u3088\u3046\u8a2d\u5b9a\u3057\u3066\u3042\u308b\u5834\u5408\u306f\u3001\u30b2\u30fc\u30e0\u3092\u30c0\u30a6\u30f3\u30ed\u30fc\u30c9\u3067\u304d\u307e\u305b\u3093\u3002  \u8a2d\u5b9a\u3092 \
6292 -  <code>javaw.exe</code>\u7d4c\u7531\u3067\u30a4\u30f3\u30bf\u30fc\u30cd\u30c3\u30c8\u306b\u30a2\u30af\u30bb\u30b9\u3067\u304d\u308b\u3088\u3046\u306b\u5909\u66f4\u3057\u3066\u304f\u3060\u3055\u3044\u3002  \u30b2\u30fc\u30e0\u3092\u518d\u8d77\u52d5 \
6293 -  \u3057\u305f\u5f8c\u3001\u30d5\u30a1\u30a4\u30a2\u30a6\u30a9\u30fc\u30eb\u306e\u8a2d\u5b9a\u304b\u3089javaw.exe \u3092\u524a\u9664 \
6294 -  \u3057\u3066\u304f\u3060\u3055\u3044\uff08\u30b9\u30bf\u30fc\u30c8\u2192\u30b3\u30f3\u30c8\u30ed\u30fc\u30eb\u30d1\u30cd\u30eb\u2192\u30d5\u30a1\u30a4\u30a2\u30a6\u30a9\u30fc\u30eb\uff09\u3002</ul> \
6295 -  <p>\u30d7\u30ed\u30ad\u30b7\u8a2d\u5b9a\u306e\u81ea\u52d5\u691c\u51fa\u304c\u3067\u304d\u307e\u305b\u3093\u3002\u304a\u4f7f\u3044\u306e\u30b3\u30f3\u30d4\u30e5\u30fc\u30bf\u30fc\u306f \
6296 -  \u30d7\u30ed\u30ad\u30b7\u3092\u4f7f\u7528\u3057\u3066\u30a4\u30f3\u30bf\u30fc\u30cd\u30c3\u30c8\u3078\u30a2\u30af\u30bb\u30b9\u3057\u3066\u3044\u307e\u3059\u3002  \u30d7\u30ed\u30ad\u30b7\u8a2d\u5b9a\u306e\u8a73\u7d30\u304c \
6297 -  \u308f\u304b\u3063\u3066\u3044\u308b\u5834\u5408\u306f\u3001\u4e0b\u306b\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002</html> 
6298 -
6299 -m.proxy_extra = <html>\u30d7\u30ed\u30ad\u30b7\u3092\u4f7f\u7528\u3057\u3066\u3044\u306a\u3044\u5834\u5408\u306f\u3001\u4e00\u6642\u7684\u306a \
6300 -  \u30a4\u30f3\u30bf\u30fc\u30cd\u30c3\u30c8\u306e\u4e0d\u5177\u5408\u306b\u3088\u308a\u3001\u30b5\u30fc\u30d0\u3068\u4ea4\u4fe1\u3067\u304d\u306a\u3044\u72b6\u614b\u306b\u3042\u308b \
6301 -  \u53ef\u80fd\u6027\u304c\u3042\u308a\u307e\u3059\u3002  \u305d\u306e\u5834\u5408\u306f\u30a4\u30f3\u30b9\u30c8\u30fc\u30eb\u3092\u30ad\u30e3\u30f3\u30bb\u30eb\u3057\u3066\u3001 \
6302 -  \u5f8c\u307b\u3069\u6539\u3081\u3066\u5b9f\u884c\u3057\u3066\u304f\u3060\u3055\u3044\u3002</html> 
6303 -
6304 -m.proxy_host = \u30d7\u30ed\u30ad\u30b7IP 
6305 -m.proxy_port = \u30d7\u30ed\u30ad\u30b7\u30dd\u30fc\u30c8
6306 +# Getdown Japanese translation messages
6307 +
6308 +m.abort_title = インストールを中止しますか?
6309 +m.abort_confirm = <html>本当にインストールを中止しますか?  \
6310 +  後でアプリケーションを起動した際にインストールを再開できます。</html>
6311 +m.abort_ok = 中止
6312 +m.abort_cancel = インストールの続行
6313 +
6314 +m.detecting_proxy = 自動プロキシ設定実行中
6315 +
6316 +m.configure_proxy = <html>サーバに接続できないため、ゲームのダウンロードに \
6317 +  失敗しました。  \
6318 +  <ul><li>ウィンドウズファイアウォールまたはノートンインターネットセキュリティが \
6319 +  <code>javaw.exe</code>をブロックするよう設定してある場合は、ゲームをダウンロードできません。  設定を \
6320 +  <code>javaw.exe</code>経由でインターネットにアクセスできるように変更してください。  ゲームを再起動 \
6321 +  した後、ファイアウォールの設定からjavaw.exe を削除 \
6322 +  してください(スタート→コントロールパネル→ファイアウォール)。</ul> \
6323 +  <p>プロキシ設定の自動検出ができません。お使いのコンピューターは \
6324 +  プロキシを使用してインターネットへアクセスしています。  プロキシ設定の詳細が \
6325 +  わかっている場合は、下に入力してください。</html>
6326 +
6327 +m.proxy_extra = <html>プロキシを使用していない場合は、一時的な \
6328 +  インターネットの不具合により、サーバと交信できない状態にある \
6329 +  可能性があります。  その場合はインストールをキャンセルして、 \
6330 +  後ほど改めて実行してください。</html>
6331 +
6332 +m.proxy_host = プロキシIP
6333 +m.proxy_port = プロキシポート
6334  m.proxy_username = Username
6335  m.proxy_password = Password
6336  m.proxy_auth_required = Authentication required
6337 -m.proxy_ok = OK  
6338 -m.proxy_cancel = \u30ad\u30e3\u30f3\u30bb\u30eb 
6339 +m.proxy_ok = OK
6340 +m.proxy_cancel = キャンセル
6341  
6342 -m.downloading_java = Java\u30d0\u30fc\u30c1\u30e3\u30eb\u30de\u30b7\u30f3\u306e\u30c0\u30a6\u30f3\u30ed\u30fc\u30c9\u4e2d
6343 -m.unpacking_java = Java\u30d0\u30fc\u30c1\u30e3\u30eb\u30de\u30b7\u30f3\u306e\u89e3\u51cd\u4e2d
6344 +m.downloading_java = Javaバーチャルマシンのダウンロード中
6345 +m.unpacking_java = Javaバーチャルマシンの解凍中
6346  
6347 -m.resolving = \u30c0\u30a6\u30f3\u30ed\u30fc\u30c9\u306e\u8a2d\u5b9a\u4e2d
6348 -m.downloading = \u30c7\u30fc\u30bf\u306e\u30c0\u30a6\u30f3\u30ed\u30fc\u30c9\u4e2d
6349 -m.failure = \u30c0\u30a6\u30f3\u30ed\u30fc\u30c9\u5931\u6557\uff1a  {0} 
6350 +m.resolving = ダウンロードの設定中
6351 +m.downloading = データのダウンロード中
6352 +m.failure = ダウンロード失敗:  {0}
6353  
6354 -m.checking = \u30a2\u30c3\u30d7\u30c7\u30fc\u30c8\u306e\u78ba\u8a8d\u4e2d
6355 -m.validating = \u8a8d\u8a3c\u4e2d
6356 -m.patching = \u4fee\u6b63\u30d7\u30ed\u30b0\u30e9\u30e0\u306e\u5b9f\u884c\u4e2d
6357 -m.launching = \u5b9f\u884c\u4e2d
6358 +m.checking = アップデートの確認中
6359 +m.validating = 認証中
6360 +m.patching = 修正プログラムの実行中
6361 +m.launching = 実行中
6362  
6363 -m.complete = {0}\uff05\u5b8c\u4e86 
6364 -m.remain = \u3000\u6b8b\u308a{0} 
6365 +m.complete = {0}%完了
6366 +m.remain =  残り{0}
6367  
6368 -m.updating_metadata = \u30b3\u30f3\u30c8\u30ed\u30fc\u30eb\u30d5\u30a1\u30a4\u30eb\u306e\u30c0\u30a6\u30f3\u30ed\u30fc\u30c9\u4e2d
6369 +m.updating_metadata = コントロールファイルのダウンロード中
6370  
6371 -m.init_failed = \u74b0\u5883\u8a2d\u5b9a\u30d5\u30a1\u30a4\u30eb\u304c\u5b58\u5728\u3057\u306a\u3044\u304b\u3001\u307e\u305f\u306f\u58ca\u308c\u3066\u3044\u307e\u3059\u3002  \u65b0\u30d0\u30fc\u30b8\u30e7\u30f3\u3092 \
6372 -  \u30c0\u30a6\u30f3\u30ed\u30fc\u30c9\u4e2d\u2026 
6373 +m.init_failed = 環境設定ファイルが存在しないか、または壊れています。  新バージョンを \
6374 +  ダウンロード中…
6375  
6376 -m.java_download_failed = \u304a\u4f7f\u3044\u306e\u30b3\u30f3\u30d4\u30e5\u30fc\u30bf\u30fc\u306b\u3001Java\u30d7\u30ed\u30b0\u30e9\u30e0\u306e\u6700\u65b0 \
6377 -  \u30d0\u30fc\u30b8\u30e7\u30f3\u3092\u81ea\u52d5\u30a4\u30f3\u30b9\u30c8\u30fc\u30eb\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002\n\n \
6378 -  www.java.com \u304b\u3089\u6700\u65b0\u30d0\u30fc\u30b8\u30e7\u30f3\u3092\u624b\u52d5\u3067\u30c0\u30a6\u30f3\u30ed\u30fc\u30c9\u3057\u3066\u3001 \
6379 -  \u518d\u5ea6\u30a2\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3\u3092\u8d77\u52d5\u3057\u3066\u304f\u3060\u3055\u3044\u3002 
6380 +m.java_download_failed = お使いのコンピューターに、Javaプログラムの最新 \
6381 +  バージョンを自動インストールできませんでした。\n\n \
6382 +  www.java.com から最新バージョンを手動でダウンロードして、 \
6383 +  再度アプリケーションを起動してください。
6384  
6385 -m.java_unpack_failed = Java\u306e\u30a2\u30c3\u30d7\u30c7\u30fc\u30c8\u30d0\u30fc\u30b8\u30e7\u30f3\u304c\u89e3\u51cd \
6386 -  \u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002  \u30cf\u30fc\u30c9\u30c9\u30e9\u30a4\u30d6\u306e\u30e1\u30e2\u30ea\u304c100MB\u4ee5\u4e0a\u3042\u308b\u3053\u3068\u3092\u78ba\u8a8d\u3057\u3066\u304b\u3089 \
6387 -  \u518d\u5ea6\u30a2\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3\u3092\u8d77\u52d5\u3057\u3066\u304f\u3060\u3055\u3044\u3002\n\n \
6388 -  \u554f\u984c\u304c\u89e3\u6c7a\u3057\u306a\u3044\u5834\u5408\u306f\u3001www.java.com \u304b\u3089Java\u306e\u6700\u65b0\u30d0\u30fc\u30b8\u30e7\u30f3\u3092 \
6389 -  \u30c0\u30a6\u30f3\u30ed\u30fc\u30c9\u3057\u3066\u304b\u3089\u3001\u518d\u5ea6\u304a\u8a66\u3057\u304f\u3060\u3055\u3044\u3002 
6390 +m.java_unpack_failed = Javaのアップデートバージョンが解凍 \
6391 +  できませんでした。  ハードドライブのメモリが100MB以上あることを確認してから \
6392 +  再度アプリケーションを起動してください。\n\n \
6393 +  問題が解決しない場合は、www.java.com からJavaの最新バージョンを \
6394 +  ダウンロードしてから、再度お試しください。
6395  
6396 -m.unable_to_repair = 5\u56de\u8a66\u884c\u3057\u307e\u3057\u305f\u304c\u3001\u5fc5\u8981\u306a\u30d5\u30a1\u30a4\u30eb\u3092\u30c0\u30a6\u30f3\u30ed\u30fc\u30c9 \
6397 -  \u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002  \u5f8c\u307b\u3069\u6539\u3081\u3066\u30a2\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3\u3092\u5b9f\u884c\u3057\u3066\u304f\u3060\u3055\u3044\u3002 \
6398 -  \u518d\u5ea6\u5931\u6557\u3057\u305f\u5834\u5408\u306f\u3001\u30a2\u30f3\u30a4\u30f3\u30b9\u30c8\u30fc\u30eb\u5f8c\u306b\u518d\u30a4\u30f3\u30b9\u30c8\u30fc\u30eb\u3057\u3066\u304f\u3060\u3055\u3044\u3002 
6399 +m.unable_to_repair = 5回試行しましたが、必要なファイルをダウンロード \
6400 +  できませんでした。  後ほど改めてアプリケーションを実行してください。 \
6401 +  再度失敗した場合は、アンインストール後に再インストールしてください。
6402  
6403 -m.unknown_error = \u539f\u56e0\u4e0d\u660e\u306e\u30a8\u30e9\u30fc\u306b\u3088\u308a\u3001\u30a2\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3\u304c \
6404 -  \u5b9f\u884c\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002  \u89e3\u6c7a\u65b9\u6cd5\u3092\n{0}\u3067 \
6405 -  \u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002 
6406 -m.init_error = \u6b21\u306e\u30a8\u30e9\u30fc\u306b\u3088\u308a\u30a2\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3\u3092\u5b9f\u884c\u3067\u304d\u307e\u305b\u3093 \
6407 -  \u3067\u3057\u305f\u3002\n{0}\n\n\u5bfe\u51e6\u65b9\u6cd5\u3092\n{1}\u3067 \
6408 -  \u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002 
6409 +m.unknown_error = 原因不明のエラーにより、アプリケーションが \
6410 +  実行できませんでした。  解決方法を\n{0}で \
6411 +  確認してください。
6412 +m.init_error = 次のエラーによりアプリケーションを実行できません \
6413 +  でした。\n{0}\n\n対処方法を\n{1}で \
6414 +  確認してください。
6415  
6416 -m.readonly_error = \u3053\u306e\u30a2\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3\u304c\u30a4\u30f3\u30b9\u30c8\u30fc\u30eb\u3055\u308c\u305f\u30d5\u30a9\u30eb\u30c0\u306f  \
6417 -  \n{0}\n\u8aad\u307f\u53d6\u308a\u5c02\u7528\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002  \u30a2\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3\u3092\u66f8\u304d\u8fbc\u307f\u304c\u3067\u304d\u308b\u30d5\u30a9\u30eb\u30c0\u306b \
6418 -  \u30a4\u30f3\u30b9\u30c8\u30fc\u30eb\u3057\u3066\u304f\u3060\u3055\u3044\u3002 
6419 +m.readonly_error = このアプリケーションがインストールされたフォルダは  \
6420 +  \n{0}\n読み取り専用に設定されています。  アプリケーションを書き込みができるフォルダに \
6421 +  インストールしてください。
6422  
6423 -m.missing_resource = \u30ea\u30bd\u30fc\u30b9\u4e0d\u660e\u306e\u305f\u3081\u30a2\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3\u3092\u5b9f\u884c\u3067\u304d\u307e\u305b\u3093 \
6424 -  \u3067\u3057\u305f\u3002\n{0}\n\n\u5bfe\u51e6\u65b9\u6cd5\u3092\n{1}\u3067\u78ba\u8a8d \
6425 -  \u3057\u3066\u304f\u3060\u3055\u3044\u3002 
6426 +m.missing_resource = リソース不明のためアプリケーションを実行できません \
6427 +  でした。\n{0}\n\n対処方法を\n{1}で確認 \
6428 +  してください。
6429  
6430 -m.insufficient_permissions_error = \u3053\u306e\u30a2\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3\u306e\u30c7\u30b8\u30bf\u30eb\u7f72\u540d\u304c\u62d2\u5426 \
6431 - \u3055\u308c\u307e\u3057\u305f\u3002  \u30a2\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3\u3092\u5b9f\u884c\u3059\u308b\u5834\u5408\u306f\u3001\u30c7\u30b8\u30bf\u30eb\u7f72\u540d\u306e\u627f\u8a8d\u304c \
6432 - \u5fc5\u8981\u3067\u3059\u3002\n\n\u627f\u8a8d\u306b\u306f\u3001\u30d6\u30e9\u30a6\u30b6\u3092\u9589\u3058\u3066\u304b\u3089\u518d\u5ea6\u958b\u304d\u3001 \
6433 - \u672c\u30db\u30fc\u30e0\u30da\u30fc\u30b8\u3092\u518d\u8868\u793a\u3057\u3066\u30a2\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3\u3092\u518d\u5ea6\u5b9f\u884c\u3057\u3066\u304f\u3060\u3055\u3044  \u30bb\u30ad\u30e5\u30ea\u30c6\u30a3\u306e \
6434 - \u8b66\u544a\u304c\u8868\u793a\u3055\u308c\u305f\u6642\u306f\u3001\u5b9f\u884c\u3092\u30af\u30ea\u30c3\u30af\u3057\u3066\u30c7\u30b8\u30bf\u30eb\u7f72\u540d\u3092\u627f\u8a8d\u3057\u3001 \
6435 - \u30a2\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3\u3092\u5b9f\u884c\u3057\u3066\u304f\u3060\u3055\u3044\u3002 
6436 +m.insufficient_permissions_error = このアプリケーションのデジタル署名が拒否 \
6437 + されました。  アプリケーションを実行する場合は、デジタル署名の承認が \
6438 + 必要です。\n\n承認には、ブラウザを閉じてから再度開き、 \
6439 + 本ホームページを再表示してアプリケーションを再度実行してください  セキュリティの \
6440 + 警告が表示された時は、実行をクリックしてデジタル署名を承認し、 \
6441 + アプリケーションを実行してください。
6442  
6443 -m.corrupt_digest_signature_error = \u30a2\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3\u306e\u30c7\u30b8\u30bf\u30eb\u7f72\u540d\u304c\u8a8d\u8a3c \
6444 - \u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002\n\u6307\u5b9a\u30db\u30fc\u30e0\u30da\u30fc\u30b8\u304b\u3089\u30a2\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3\u3092\u5b9f\u884c\u3057\u3066\u3044\u308b\u304b\n \
6445 - \u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002 
6446 +m.corrupt_digest_signature_error = アプリケーションのデジタル署名が認証 \
6447 + できませんでした。\n指定ホームページからアプリケーションを実行しているか\n \
6448 + 確認してください。
6449  
6450 -m.default_install_error = \u30db\u30fc\u30e0\u30da\u30fc\u30b8\u3067\u306e\u30b5\u30dd\u30fc\u30c8\u8868\u793a 
6451 +m.default_install_error = ホームページでのサポート表示
6452  
6453  # application/digest errors
6454 -m.missing_appbase = \u8a2d\u5b9a\u30d5\u30a1\u30a4\u30eb\u306eappbase\u304c\u4e0d\u660e\u3067\u3059\u3002 
6455 -m.invalid_version = \u8a2d\u5b9a\u30d5\u30a1\u30a4\u30eb\u306f\u7121\u52b9\u306a\u30d0\u30fc\u30b8\u30e7\u30f3\u3092\u6307\u5b9a\u3057\u3066\u3044\u307e\u3059\u3002 
6456 -m.invalid_appbase = \u8a2d\u5b9a\u30d5\u30a1\u30a4\u30eb\u304c\u7121\u52b9\u306aappbase\u3092\u6307\u5b9a\u3057\u3066\u3044\u307e\u3059\u3002 
6457 -m.missing_class = \u8a2d\u5b9a\u30d5\u30a1\u30a4\u30eb\u306e\u30a2\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3\u30af\u30e9\u30b9\u304c\u4e0d\u660e\u3067\u3059\u3002 
6458 -m.missing_code = \u8a2d\u5b9a\u30d5\u30a1\u30a4\u30eb\u3067\u30b3\u30fc\u30c9\u30ea\u30bd\u30fc\u30b9\u304c\u6307\u5b9a\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002 
6459 -m.invalid_digest_file = \u30c0\u30a4\u30b8\u30a7\u30b9\u30c8\u30d5\u30a1\u30a4\u30eb\u304c\u7121\u52b9\u3067\u3059\u3002 
6460 +m.missing_appbase = 設定ファイルのappbaseが不明です。
6461 +m.invalid_version = 設定ファイルは無効なバージョンを指定しています。
6462 +m.invalid_appbase = 設定ファイルが無効なappbaseを指定しています。
6463 +m.missing_class = 設定ファイルのアプリケーションクラスが不明です。
6464 +m.missing_code = 設定ファイルでコードリソースが指定されていません。
6465 +m.invalid_digest_file = ダイジェストファイルが無効です。
6466 diff --git a/getdown/src/getdown/launcher/src/main/resources/com/threerings/getdown/messages_ko.properties b/getdown/src/getdown/launcher/src/main/resources/com/threerings/getdown/messages_ko.properties
6467 index 3f8a47f35..05700d363 100644
6468 --- a/getdown/src/getdown/launcher/src/main/resources/com/threerings/getdown/messages_ko.properties
6469 +++ b/getdown/src/getdown/launcher/src/main/resources/com/threerings/getdown/messages_ko.properties
6470 @@ -1,102 +1,96 @@
6471  #
6472 -# $Id$
6473 -#
6474 -# Getdown translation messages
6475 +# Getdown Korean translation messages
6476  
6477 -m.abort_title = \uC124\uCE58\uB97C \uCDE8\uC18C\uD558\uC2DC\uACA0\uC2B5\uB2C8\uAE4C?
6478 -m.abort_confirm = <html>\uC815\uB9D0\uB85C \uC124\uCE58\uB97C \uCDE8\uC18C\uD558\uC2DC\uACA0\uC2B5\uB2C8\uAE4C? \
6479 -  \uB098\uC911\uC5D0 \uC5B4\uD50C\uB9AC\uCF00\uC774\uC158\uC744 \uC2E4\uD589\uD558\uC5EC \uC124\uCE58\uB97C \uC7AC\uAC1C\uD558\uC5EC \uC8FC\uC2ED\uC2DC\uC624.</html>
6480 -m.abort_ok = \uC911\uC9C0
6481 -m.abort_cancel = \uACC4\uC18D\uD558\uC5EC \uC124\uCE58
6482 +m.abort_title = 설치를 취소하시겠습니까?
6483 +m.abort_confirm = <html>정말로 설치를 취소하시겠습니까? \
6484 +  나중에 어플리케이션을 실행하여 설치를 재개하여 주십시오.</html>
6485 +m.abort_ok = 중지
6486 +m.abort_cancel = 계속하여 설치
6487  
6488 -m.detecting_proxy = \uC790\uB3D9 \uD504\uB85D\uC2DC\uB97C \uC124\uC815\uC744 \uC2DC\uB3C4
6489 +m.detecting_proxy = 자동 프록시를 설정을 시도
6490  
6491 -m.configure_proxy = <html>\uAC8C\uC784 \uB370\uC774\uD130\uB97C \uBC1B\uAE30 \uC704\uD55C \uC11C\uBC84 \uC811\uC18D\uC5D0 \uC2E4\uD328\uD558\uC600\uC2B5\uB2C8\uB2E4.\
6492 -  <ul><li>\uC708\uB3C4\uC6B0 \uBC29\uD654\uBCBD \uB610\uB294 \uB178\uD134 \uC778\uD130\uB137 \uC2DC\uD050\uB9AC\uD2F0\uAC00 <code>javaw.exe</code>\uC774 \uC124\uC815\uC5D0\uC11C \uCC28\uB2E8\uB418\uC5B4 \uC788\uC744 \uACBD\uC6B0, \
6493 -  \uAC8C\uC784 \uB370\uC774\uD130\uB97C \uB2E4\uC6B4\uB85C\uB4DC \uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. \
6494 -  <code>javaw.exe</code>\uAC00 \uC778\uD130\uB137 \uC5F0\uACB0\uC744 \uD560 \uC218 \uC788\uB3C4\uB85D \uC124\uC815\uC744 \uBCC0\uACBD\uD558\uC5EC \uC8FC\uC2ED\uC2DC\uC624. \
6495 -  \uAC8C\uC784\uC744 \uB2E4\uC2DC \uC2E4\uD589\uD55C \uD6C4, \uBC29\uD654\uBCBD \uC124\uC815\uC5D0\uC11C javaw.exe\uB97C \uC0AD\uC81C\uD558\uC5EC \uC8FC\uC2ED\uC2DC\uC624. \
6496 -  ( \uC2DC\uC791 -> \uC81C\uC5B4\uD310 -> \uC708\uB3C4\uC6B0 \uBC29\uD654\uBCBD )</ul> \
6497 -  <p> \uCEF4\uD4E8\uD130\uAC00 \uD504\uB85D\uC2DC \uC11C\uBC84\uB97C \uD1B5\uD574 \uC778\uD130\uB137\uC5D0 \uC5F0\uACB0\uB418\uC5B4 \uC788\uB2E4\uBA74, \uD504\uB85D\uC2DC \uC124\uC815\uC758 \uC790\uB3D9 \uAD6C\uC131\uC744 \uC0AC\uC6A9\uD560 \uC218 \uC5C6\uC73C\uBBC0\uB85C, \
6498 -  \uC0AC\uC6A9\uD558\uB294 \uD504\uB85D\uC2DC \uC124\uC815\uC744 \uC54C\uACE0 \uC788\uC744 \uACBD\uC6B0 \uC544\uB798\uC5D0 \uC785\uB825\uD558\uC5EC \uC8FC\uC2DC\uAE38 \uBC14\uB78D\uB2C8\uB2E4.</html>
6499 +m.configure_proxy = <html>게임 데이터를 받기 위한 서버 접속에 실패하였습니다.\
6500 +  <ul><li>윈도우 방화벽 또는 노턴 인터넷 시큐리티가 <code>javaw.exe</code>이 설정에서 차단되어 있을 경우, \
6501 +  게임 데이터를 다운로드 할 수 없습니다. \
6502 +  <code>javaw.exe</code>가 인터넷 연결을 할 수 있도록 설정을 변경하여 주십시오. \
6503 +  게임을 다시 실행한 후, 방화벽 설정에서 javaw.exe를 삭제하여 주십시오. \
6504 +  ( 시작 -> 제어판 -> 윈도우 방화벽 )</ul> \
6505 +  <p> 컴퓨터가 프록시 서버를 통해 인터넷에 연결되어 있다면, 프록시 설정의 자동 구성을 사용할 수 없으므로, \
6506 +  사용하는 프록시 설정을 알고 있을 경우 아래에 입력하여 주시길 바랍니다.</html>
6507  
6508 -m.proxy_extra = \uC790\uB3D9 \uD504\uB85D\uC2DC\uB97C \uC124\uC815\uC744 \uC2DC\uB3C4
6509 +m.proxy_extra = 자동 프록시를 설정을 시도
6510  
6511 -m.proxy_host = \uD504\uB85D\uC2DC IP
6512 -m.proxy_port = \uD504\uB85D\uC2DC \uD3EC\uD2B8
6513 +m.proxy_host = 프록시 IP
6514 +m.proxy_port = 프록시 포트
6515  m.proxy_username = Username
6516  m.proxy_password = Password
6517  m.proxy_auth_required = Authentication required
6518  m.proxy_ok = OK
6519 -m.proxy_cancel = \uCDE8\uC18C
6520 -
6521 -m.downloading_java = \uC790\uBC14 \uAC00\uC0C1 \uBA38\uC2E0(JVM) \uB2E4\uC6B4\uB85C\uB4DC \uC911
6522 -m.unpacking_java = \uC790\uBC14 \uAC00\uC0C1 \uBA38\uC2E0(JVM) \uC555\uCD95\uC744 \uD574\uC81C\uD558\uB294 \uC911
6523 -
6524 -m.resolving = \uB2E4\uC6B4\uB85C\uB4DC \uBD84\uC11D \uC911
6525 -m.downloading = \uB370\uC774\uD130 \uB2E4\uC6B4\uB85C\uB4DC \uC911
6526 -m.failure = \uB2E4\uC6B4\uB85C\uB4DC \uC2E4\uD328: {0}
6527 +m.proxy_cancel = 취소
6528  
6529 -m.checking = \uC5C5\uB370\uC774\uD2B8 \uCCB4\uD06C
6530 -m.validating = \uC720\uD6A8\uC131 \uAC80\uC0AC \uC911
6531 -m.patching = \uD328\uCE58 \uC911
6532 -m.launching = \uC2E4\uD589 \uC911
6533 +m.downloading_java = 자바 가상 머신(JVM) 다운로드 중
6534 +m.unpacking_java = 자바 가상 머신(JVM) 압축을 해제하는 중
6535  
6536 -m.patch_notes = \uD328\uCE58 \uB178\uD2B8
6537 -m.play_again = \uB2E4\uC2DC \uC2E4\uD589
6538 +m.resolving = 다운로드 분석 중
6539 +m.downloading = 데이터 다운로드 중
6540 +m.failure = 다운로드 실패: {0}
6541  
6542 -m.complete = {0}% \uC644\uB8CC
6543 -m.remain = {0} \uB0A8\uC74C
6544 +m.checking = 업데이트 체크
6545 +m.validating = 유효성 검사 중
6546 +m.patching = 패치 중
6547 +m.launching = 실행 중
6548  
6549 -m.updating_metadata = \uCEE8\uD2B8\uB864 \uD30C\uC77C\uC744 \uB2E4\uC6B4\uB85C\uB4DC \uC911
6550 +m.patch_notes = 패치 노트
6551 +m.complete = {0}% 완료
6552 +m.remain = {0} 남음
6553  
6554 -m.init_failed = \uC124\uC815 \uD30C\uC77C\uC774 \uB204\uB77D\uB418\uC5C8\uAC70\uB098 \uBCC0\uD615\uB418\uC5C8\uC2B5\uB2C8\uB2E4. \
6555 -  \uC0C8\uB85C\uC6B4 \uBCF5\uC0AC\uBCF8\uC744 \uB2E4\uC6B4\uB85C\uB4DC \uC911\uC785\uB2C8\uB2E4...
6556 +m.updating_metadata = 컨트롤 파일을 다운로드 중
6557  
6558 -m.java_download_failed = \uC774 \uCEF4\uD4E8\uD130\uC5D0 \uD544\uC694\uD55C \uC0C8\uB85C\uC6B4 \uBC84\uC804\uC758 \uC790\uBC14\uB97C \uC790\uB3D9\uC73C\uB85C \uB2E4\uC6B4\uB85C\uB4DC\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.\n\n\
6559 -  \uC790\uBC14 \uC6F9\uC0AC\uC774\uD2B8(www.java.com)\uB85C \uAC00\uC11C \uCD5C\uC2E0\uC758 \uC790\uBC14\uB97C \uB2E4\uC6B4\uB85C\uB4DC \uBC1B\uC73C\uC2E0 \uD6C4, \
6560 -  \uC5B4\uD50C\uB9AC\uCF00\uC774\uC158\uC744 \uB2E4\uC2DC \uC2E4\uD589\uD574 \uC8FC\uC2ED\uC2DC\uC624.
6561 +m.init_failed = 설정 파일이 누락되었거나 변형되었습니다. \
6562 +  새로운 복사본을 다운로드 중입니다...
6563  
6564 -m.java_unpack_failed = \uC5C5\uB370\uC774\uD2B8\uB41C \uBC84\uC804\uC758 \uC790\uBC14\uC758 \uC555\uCD95\uC744 \uD480 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. \
6565 -  \uD558\uB4DC\uB4DC\uB77C\uC774\uBE0C\uC5D0 \uCD5C\uC18C\uD55C 100MB\uC758 \uC6A9\uB7C9\uC744 \uD655\uBCF4\uD55C \uC774\uD6C4, \uC5B4\uD50C\uB9AC\uCF00\uC774\uC158\uC744 \uB2E4\uC2DC \uC2E4\uD589\uD574 \uC8FC\uC2ED\uC2DC\uC624.\n\n\
6566 -  \uB9CC\uC57D \uBB38\uC81C\uAC00 \uD574\uACB0\uB418\uC9C0 \uC54A\uB294\uB2E4\uBA74, \uC790\uBC14 \uC6F9\uC0AC\uC774\uD2B8(www.java.com)\uB85C \uAC00\uC11C \uCD5C\uC2E0\uC758 \uC790\uBC14\uB97C \uB2E4\uC6B4\uB85C\uB4DC \uBC1B\uC73C\uC2E0 \uD6C4, \
6567 -  \uC5B4\uD50C\uB9AC\uCF00\uC774\uC158\uC744 \uB2E4\uC2DC \uC2E4\uD589\uD574 \uC8FC\uC2ED\uC2DC\uC624.
6568 +m.java_download_failed = 이 컴퓨터에 필요한 새로운 버전의 자바를 자동으로 다운로드할 수 없습니다.\n\n\
6569 +  자바 웹사이트(www.java.com)로 가서 최신의 자바를 다운로드 받으신 후, \
6570 +  어플리케이션을 다시 실행해 주십시오.
6571  
6572 -m.unable_to_repair = \uB2E4\uC12F\uBC88\uC758 \uC2DC\uB3C4\uC5D0\uB3C4 \uD544\uC694\uD55C \uD30C\uC77C\uC744 \uB2E4\uC6B4\uB85C\uB4DC\uD558\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4. \
6573 -  \uC5B4\uD50C\uB9AC\uCF00\uC774\uC158\uC744 \uB2E4\uC2DC \uC2DC\uC791\uD574\uBCF4\uC2DC\uACE0, \uADF8\uB798\uB3C4 \uB2E4\uC6B4\uB85C\uB4DC\uC5D0 \uC2E4\uD328\uD55C\uB2E4\uBA74, \uC5B4\uD50C\uB9AC\uCF00\uC774\uC158\uC744 \uC81C\uAC70\uD55C \uD6C4, \uB2E4\uC2DC \uC2E4\uD589\uD574\uBCF4\uC2DC\uAE30 \uBC14\uB78D\uB2C8\uB2E4.
6574 +m.java_unpack_failed = 업데이트된 버전의 자바의 압축을 풀 수 없습니다. \
6575 +  하드드라이브에 최소한 100MB의 용량을 확보한 이후, 어플리케이션을 다시 실행해 주십시오.\n\n\
6576 +  만약 문제가 해결되지 않는다면, 자바 웹사이트(www.java.com)로 가서 최신의 자바를 다운로드 받으신 후, \
6577 +  어플리케이션을 다시 실행해 주십시오.
6578  
6579 -m.unknown_error = \uBCF5\uAD6C\uB420 \uC218 \uC5C6\uB294 \uC624\uB958\uB85C \uC778\uD558\uC5EC \uC5B4\uD50C\uB9AC\uCF00\uC774\uC158\uC758 \uC2E4\uD589\uC774 \uC911\uB2E8\uB418\uC5C8\uC2B5\uB2C8\uB2E4. \
6580 -  \n{0}\uC5D0 \uB300\uD55C \uBCF5\uAD6C \uBC29\uBC95\uC744 \uCC3E\uAE30 \uC704\uD574\uC11C \uBC29\uBB38\uD558\uC2DC\uAE38 \uBC14\uB78D\uB2C8\uB2E4.
6581 +m.unable_to_repair = 다섯번의 시도에도 필요한 파일을 다운로드하지 못했습니다. \
6582 +  어플리케이션을 다시 시작해보시고, 그래도 다운로드에 실패한다면, 어플리케이션을 제거한 후, 다시 실행해보시기 바랍니다.
6583  
6584 -m.init_error = \uC5B4\uD50C\uB9AC\uCF00\uC774\uC158\uC774 \uC544\uB798\uC640 \uAC19\uC740 \uC5D0\uB7EC\uB85C \uC2E4\uD589\uC774 \uC911\uB2E8\uB418\uC5C8\uC2B5\uB2C8\uB2E4. \uC5D0\uB7EC:\
6585 -  \n{0}\n\n{1}\uC5D0 \uB300\uD55C \uBB38\uC81C \uD574\uACB0 \uBC29\uBC95\uC744 \uCC3E\uAE30 \uC704\uD574\uC11C \uBC29\uBB38\uD558\uC2DC\uAE38 \uBC14\uB78D\uB2C8\uB2E4.
6586 +m.unknown_error = 복구될 수 없는 오류로 인하여 어플리케이션의 실행이 중단되었습니다. \
6587 +  \n{0}에 대한 복구 방법을 찾기 위해서 방문하시길 바랍니다.
6588  
6589 -m.readonly_error = \uC5B4\uD50C\uB9AC\uCF00\uC774\uC158\uC774 \uC124\uCE58\uB41C \uB514\uB809\uD1A0\uB9AC: \
6590 -  \n{0}\n\uAC00 \uC77D\uAE30 \uC804\uC6A9\uC785\uB2C8\uB2E4. \uC77D\uAE30 \uAD8C\uD55C\uC774 \uC2B9\uC778\uB41C \uB809\uD1A0\uB9AC\uC5D0 \uC5B4\uD50C\uB9AC\uCF00\uC774\uC158\uC744 \uC124\uCE58\uD558\uC2DC\uAE38 \uBC14\uB78D\uB2C8\uB2E4.
6591 +m.init_error = 어플리케이션이 아래와 같은 에러로 실행이 중단되었습니다. 에러:\
6592 +  \n{0}\n\n{1}에 대한 문제 해결 방법을 찾기 위해서 방문하시길 바랍니다.
6593  
6594 -m.missing_resource = \uB9AC\uC18C\uC2A4\uC758 \uC190\uC2E4\uB85C \uC778\uD558\uC5EC \uC5B4\uD50C\uB9AC\uCF00\uC774\uC158\uC758 \uC2E4\uD589\uC774 \uC911\uB2E8\uB418\uC5C8\uC2B5\uB2C8\uB2E4. : \
6595 -  \n{0}\n\n{1}\uC5D0 \uB300\uD55C \uBB38\uC81C \uD574\uACB0 \uBC29\uBC95\uC744 \uCC3E\uAE30 \uC704\uD574\uC11C \uBC29\uBB38\uD558\uC2DC\uAE38 \uBC14\uB78D\uB2C8\uB2E4.
6596 +m.readonly_error = 어플리케이션이 설치된 디렉토리: \
6597 +  \n{0}\n가 읽기 전용입니다. 읽기 권한이 승인된 렉토리에 어플리케이션을 설치하시길 바랍니다.
6598  
6599 -m.insufficient_permissions_error = \uC774 \uC5B4\uD50C\uB9AC\uCF00\uC774\uC158\uC758 \uB514\uC9C0\uD0C8 \uC11C\uBA85\uC744 \uD655\uC778\uD558\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4. \
6600 - \uC5B4\uD50C\uB9AC\uCF00\uC774\uC158\uC744 \uC2E4\uD589\uD558\uAE30 \uC704\uD574\uC11C \uB514\uC9C0\uD0C8 \uC11C\uBA85\uC744 \uD655\uC778\uD558\uC5EC \uC8FC\uC2ED\uC2DC\uC624. \
6601 - \n\n\uADF8\uB9AC\uACE0 \uB098\uC11C \uC6F9 \uBE0C\uB77C\uC6B0\uC800\uB97C \uB2EB\uACE0 \uB2E4\uC2DC \uC2DC\uC791\uD558\uC5EC \uC6F9\uD398\uC774\uC9C0\uB85C \uB3CC\uC544\uC640 \uC5B4\uD50C\uB9AC\uCF00\uC774\uC158\uC744 \uC7AC\uC2DC\uC791\uD574\uC8FC\uC2DC\uAE30 \uBC14\uB78D\uB2C8\uB2E4. \
6602 - \uBCF4\uC548\uC5D0 \uB300\uD55C \uB300\uD654\uC0C1\uC790\uAC00 \uBCF4\uC774\uBA74, \uB514\uC9C0\uD0C8 \uC11C\uBA85\uC5D0 \uB300\uD55C \uD655\uC778 \uBC84\uD2BC\uC744 \uD074\uB9AD\uD558\uACE0, \uC5B4\uD50C\uB9AC\uCF00\uC774\uC158\uC774 \uC2E4\uD589\uB418\uAE30 \uC704\uD55C \
6603 - \uAD8C\uD55C\uC744 \uBD80\uC5EC\uD574\uC8FC\uC2DC\uAE30 \uBC14\uB78D\uB2C8\uB2E4.
6604 +m.missing_resource = 리소스의 손실로 인하여 어플리케이션의 실행이 중단되었습니다. : \
6605 +  \n{0}\n\n{1}에 대한 문제 해결 방법을 찾기 위해서 방문하시길 바랍니다.
6606  
6607 -m.corrupt_digest_signature_error = \uC5B4\uD50C\uB9AC\uCF00\uC774\uC158\uC758 \uB514\uC9C0\uD0C8 \uC11C\uBA85\uC744 \uD655\uC778\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.\n \
6608 - \uC62C\uBC14\uB978 \uC6F9\uC0AC\uC774\uD2B8\uC5D0\uC11C \uC5B4\uD50C\uB9AC\uCF00\uC774\uC158\uC774 \uC2E4\uD589\uB418\uACE0 \uC788\uB294 \uC9C0 \uD655\uC778\uBC14\uB78D\uB2C8\uB2E4.
6609 +m.insufficient_permissions_error = 이 어플리케이션의 디지탈 서명을 확인하지 않았습니다. \
6610 + 어플리케이션을 실행하기 위해서 디지탈 서명을 확인하여 주십시오. \
6611 + \n\n그리고 나서 웹 브라우저를 닫고 다시 시작하여 웹페이지로 돌아와 어플리케이션을 재시작해주시기 바랍니다. \
6612 + 보안에 대한 대화상자가 보이면, 디지탈 서명에 대한 확인 버튼을 클릭하고, 어플리케이션이 실행되기 위한 \
6613 + 권한을 부여해주시기 바랍니다.
6614  
6615 -m.default_install_error = \uC6F9\uC0AC\uC774\uD2B8\uC758 \uC9C0\uC6D0 \uBA54\uB274(support section)\uB97C \uD655\uC778\uD558\uC2DC\uAE30 \uBC14\uB78D\uB2C8\uB2E4.
6616 +m.corrupt_digest_signature_error = 어플리케이션의 디지탈 서명을 확인할 수 없습니다.\n \
6617 + 올바른 웹사이트에서 어플리케이션이 실행되고 있는 지 확인바랍니다.
6618  
6619 -m.another_getdown_running = \uC774 \uC5B4\uD50C\uB9AC\uCF00\uC774\uC158 \uC778\uC2A4\uD1A8\uB7EC\uC758 \uB2E4\uC911 \uC778\uC2A4\uD134\uC2A4\uAC00 \uC2E4\uD589\uC911\uC785\uB2C8\uB2E4. \
6620 - \uD558\uB098\uAC00 \uC644\uB8CC\uB420 \uB54C\uAE4C\uC9C0 \uC911\uB2E8\uB429\uB2C8\uB2E4.
6621 +m.default_install_error = 웹사이트의 지원 메뉴(support section)를 확인하시기 바랍니다.
6622  
6623 -m.applet_stopped = Getdown \uC560\uD50C\uB9BF \uC2E4\uD589\uC774 \uC911\uB2E8\uB418\uC5C8\uC2B5\uB2C8\uB2E4.
6624 +m.another_getdown_running = 이 어플리케이션 인스톨러의 다중 인스턴스가 실행중입니다. \
6625 + 하나가 완료될 때까지 중단됩니다.
6626  
6627  # application/digest errors
6628 -m.missing_appbase = \uC124\uC815 \uD30C\uC77C\uC5D0 'appbase' \uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.
6629 -m.invalid_version = \uC124\uC815 \uD30C\uC77C\uC5D0 \uC798\uBABB\uB41C \uBC84\uC804\uC774 \uBA85\uC2DC\uB418\uC5B4 \uC788\uC2B5\uB2C8\uB2E4.
6630 -m.invalid_appbase = \uC124\uC815 \uD30C\uC77C\uC5D0 \uC798\uBABB\uB41C 'appbase'\uAC00 \uBA85\uC2DC\uB418\uC5B4 \uC788\uC2B5\uB2C8\uB2E4.
6631 -m.missing_class = \uC124\uC815 \uD30C\uC77C\uC5D0 \uC5B4\uD50C\uB9AC\uCF00\uC774\uC158 \uD074\uB798\uC2A4\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.
6632 -m.missing_code = \uC124\uC815 \uD30C\uC77C\uC5D0 \uB9AC\uC18C\uC2A4\uC5D0 \uB300\uD55C \uCF54\uB4DC\uAC00 \uBA85\uC2DC\uB418\uC5B4 \uC788\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4.
6633 -m.invalid_digest_file = \uB2E4\uC774\uC81C\uC2A4\uD2B8 \uD30C\uC77C\uC774 \uC798\uBABB\uB418\uC5C8\uC2B5\uB2C8\uB2E4.
6634 +m.missing_appbase = 설정 파일에 'appbase' 가 없습니다.
6635 +m.invalid_version = 설정 파일에 잘못된 버전이 명시되어 있습니다.
6636 +m.invalid_appbase = 설정 파일에 잘못된 'appbase'가 명시되어 있습니다.
6637 +m.missing_class = 설정 파일에 어플리케이션 클래스가 없습니다.
6638 +m.missing_code = 설정 파일에 리소스에 대한 코드가 명시되어 있지 않습니다.
6639 +m.invalid_digest_file = 다이제스트 파일이 잘못되었습니다.
6640 diff --git a/getdown/src/getdown/launcher/src/main/resources/com/threerings/getdown/messages_pt.properties b/getdown/src/getdown/launcher/src/main/resources/com/threerings/getdown/messages_pt.properties
6641 index 47db91c90..e59ed20b2 100644
6642 --- a/getdown/src/getdown/launcher/src/main/resources/com/threerings/getdown/messages_pt.properties
6643 +++ b/getdown/src/getdown/launcher/src/main/resources/com/threerings/getdown/messages_pt.properties
6644 @@ -1,118 +1,112 @@
6645  #
6646 -# $Id$
6647 -#
6648 -# Getdown translation messages
6649 +# Getdown Portuguese translation messages
6650  
6651 -m.abort_title = Cancelar a instala\u00E7\u00E3o?
6652 -m.abort_confirm = <html>Tem certeza que deseja cancelar a instala\u00E7\u00E3o? \
6653 -  Voc\u00EA pode continuar a instala\u00E7\u00E3o mais tarde, \
6654 -  basta executar a aplica\u00E7\u00E3o novamente.</html>
6655 +m.abort_title = Cancelar a instalação?
6656 +m.abort_confirm = <html>Tem certeza que deseja cancelar a instalação? \
6657 +  Você pode continuar a instalação mais tarde, \
6658 +  basta executar a aplicação novamente.</html>
6659  m.abort_ok = Sair
6660 -m.abort_cancel = Continuar a instala\u00E7\u00E3o
6661 +m.abort_cancel = Continuar a instalação
6662  
6663 -m.detecting_proxy = Tentando detectar automaticamente as configura\u00E7\u00F5es de proxy
6664 +m.detecting_proxy = Tentando detectar automaticamente as configurações de proxy
6665  
6666 -m.configure_proxy = <html>N\u00E3o foi poss\u00EDvel conectar aos nossos servidores para \
6667 +m.configure_proxy = <html>Não foi possível conectar aos nossos servidores para \
6668    fazer o download dos dados. \
6669 -  <ul><li> Se o Firewall do Windows ou o Norton Internet Security est\u00E1 configurado \
6670 -  para bloquear o programa <code>javaw.exe</code> n\u00E3o ser\u00E1 poss\u00EDvel realizar \
6671 -  o download. Voc\u00EA ter\u00E1 que permitir que o programa <code>javaw.exe</code> acesse \
6672 -  a internet. Voc\u00EA pode tentar executar o programa novamente, mas voc\u00EA precisa \
6673 -  remover o programa javaw.exe das configura\u00E7\u00F5es do firewall (Iniciar -> Painel \
6674 +  <ul><li> Se o Firewall do Windows ou o Norton Internet Security está configurado \
6675 +  para bloquear o programa <code>javaw.exe</code> não será possível realizar \
6676 +  o download. Você terá que permitir que o programa <code>javaw.exe</code> acesse \
6677 +  a internet. Você pode tentar executar o programa novamente, mas você precisa \
6678 +  remover o programa javaw.exe das configurações do firewall (Iniciar -> Painel \
6679    de controle -> Firewall do Windows).</ul> \
6680 -  <p> Seu computador pode estar acessando a internet atrav\u00E9s de um proxy e n\u00E3o foi \
6681 -  capaz de detectar automaticamente as configura\u00E7\u00F5es de proxy. \
6682 -  Voc\u00EA pode informar esses dados abaixo.</html>
6683 +  <p> Seu computador pode estar acessando a internet através de um proxy e não foi \
6684 +  capaz de detectar automaticamente as configurações de proxy. \
6685 +  Você pode informar esses dados abaixo.</html>
6686  
6687 -m.proxy_extra = <html>Se voc\u00EA tem certeza que n\u00E3o usa um proxy, ent\u00E3o pode ser \
6688 -  que exista um problema tempor\u00E1rio que est\u00E1 impedindo a comunica\u00E7\u00E3o \
6689 -  com os nossos servidores. Neste caso voc\u00EA pode cancelar e tentar instalar novamente \
6690 +m.proxy_extra = <html>Se você tem certeza que não usa um proxy, então pode ser \
6691 +  que exista um problema temporário que está impedindo a comunicação \
6692 +  com os nossos servidores. Neste caso você pode cancelar e tentar instalar novamente \
6693    mais tarde.</html>
6694  
6695  m.proxy_host = IP do Proxy
6696  m.proxy_port = Porta do Proxy
6697 -m.proxy_username = Nome de usu\u00e1rio
6698 +m.proxy_username = Nome de usuário
6699  m.proxy_password = Senha
6700 -m.proxy_auth_required = Autentifica\u00e7\u00e3o requerida
6701 +m.proxy_auth_required = Autentificação requerida
6702  m.proxy_ok = OK
6703  m.proxy_cancel = Cancelar
6704  
6705 -m.downloading_java = Fazendo o download da m\u00E1quina virtual Java
6706 -m.unpacking_java = Descompactando a m\u00E1quina virtual Java
6707 +m.downloading_java = Fazendo o download da máquina virtual Java
6708 +m.unpacking_java = Descompactando a máquina virtual Java
6709  
6710  m.resolving = Resolvendo downloads
6711  m.downloading = Transferindo dados
6712  m.failure = Download falhou: {0}
6713  
6714 -m.checking = Verificando atualiza\u00E7\u00F5es
6715 +m.checking = Verificando atualizações
6716  m.validating = Validando
6717  m.patching = Atualizando
6718  m.launching = Executando
6719  
6720  m.patch_notes = Corrigir notas
6721 -m.play_again = Jogar de novo
6722 -
6723  m.complete = {0}% completo
6724  m.remain = {0} Permanecer
6725  
6726  m.updating_metadata = Transferindo arquivos de controle
6727  
6728 -m.init_failed = Nosso arquivo de configura\u00E7\u00E3o est\u00E1 ausente ou corrompido. Tente \
6729 -  baixar uma nova c\u00F3pia...
6730 +m.init_failed = Nosso arquivo de configuração está ausente ou corrompido. Tente \
6731 +  baixar uma nova cópia...
6732  
6733 -m.java_download_failed = N\u00E3o conseguimos baixar automaticamente a\
6734 -  vers\u00E3o necess\u00E1ria do Java para o seu computador.\n\n\
6735 -  Por favor, acesse www.java.com, baixe e instale a \u00FAltima vers\u00E3o do \
6736 +m.java_download_failed = Não conseguimos baixar automaticamente a\
6737 +  versão necessária do Java para o seu computador.\n\n\
6738 +  Por favor, acesse www.java.com, baixe e instale a última versão do \
6739    Java, em seguida, tente executar o aplicativo novamente.
6740  
6741 -m.java_unpack_failed = N\u00E3o conseguimos descompactar uma vers\u00E3o atualizada do \
6742 -  Java. Por favor, certifique-se de ter pelo menos 100 MB de espa\u00E7o livre em seu \
6743 -  disco r\u00EDgido e tente executar o aplicativo novamente. \n\n\
6744 -  Se isso n\u00E3o resolver o problema, acesse www.java.com,baixe e \
6745 -  instale a \u00FAltima vers\u00E3o do Java e tente novamente.
6746 +m.java_unpack_failed = Não conseguimos descompactar uma versão atualizada do \
6747 +  Java. Por favor, certifique-se de ter pelo menos 100 MB de espaço livre em seu \
6748 +  disco rígido e tente executar o aplicativo novamente. \n\n\
6749 +  Se isso não resolver o problema, acesse www.java.com,baixe e \
6750 +  instale a última versão do Java e tente novamente.
6751  
6752 -m.unable_to_repair = N\u00E3o conseguimos baixar os arquivos necess\u00E1rios depois de \
6753 -  cinco tentativas. Voc\u00EA pode tentar executar o aplicativo novamente, mas se ele \
6754 -  falhar pode ser necess\u00E1rio desinstalar e reinstalar.
6755 +m.unable_to_repair = Não conseguimos baixar os arquivos necessários depois de \
6756 +  cinco tentativas. Você pode tentar executar o aplicativo novamente, mas se ele \
6757 +  falhar pode ser necessário desinstalar e reinstalar.
6758  
6759 -m.unknown_error = A aplica\u00E7\u00E3o falhou ao iniciar devido a algum erro estranho \
6760 -  do qual n\u00E3o conseguimos recuperar. Por favor, visite \n{0} para obter \
6761 -  informa\u00E7\u00F5es sobre como recuperar.
6762 -m.init_error = A aplica\u00E7\u00E3o falhou ao iniciar devido ao seguinte \
6763 +m.unknown_error = A aplicação falhou ao iniciar devido a algum erro estranho \
6764 +  do qual não conseguimos recuperar. Por favor, visite \n{0} para obter \
6765 +  informações sobre como recuperar.
6766 +m.init_error = A aplicação falhou ao iniciar devido ao seguinte \
6767    erro:\n{0}\n\nPor favor visite \n{1} para \
6768 -  informa\u00E7\u00F5es sobre como lidar com esse problema.
6769 +  informações sobre como lidar com esse problema.
6770  
6771 -m.readonly_error =O diret\u00F3rio no qual este aplicativo est\u00E1 instalado: \
6772 -  \n{0}\n \u00E9 somente leitura. Por favor, instale o aplicativo em um diret\u00F3rio onde \
6773 -  voc\u00EA tem acesso de grava\u00E7\u00E3o.
6774 +m.readonly_error =O diretório no qual este aplicativo está instalado: \
6775 +  \n{0}\n é somente leitura. Por favor, instale o aplicativo em um diretório onde \
6776 +  você tem acesso de gravação.
6777  
6778 -m.missing_resource = A aplica\u00E7\u00E3o falhou ao iniciar devido a uma falta \
6779 -  de recurso:\n{0}\n\n Por favor, visite\n{1} para obter informa\u00E7\u00F5es sobre \
6780 +m.missing_resource = A aplicação falhou ao iniciar devido a uma falta \
6781 +  de recurso:\n{0}\n\n Por favor, visite\n{1} para obter informações sobre \
6782    como lidar com tal problema.
6783  
6784 -m.insufficient_permissions_error = Voc\u00EA n\u00E3o aceitou a assinatura digital \
6785 -  do aplicativo. Se voc\u00EA quiser executar o aplicativo, voc\u00EA ter\u00E1 que aceitar \
6786 -  a assinatura digital. \n\nPara fazer isso, voc\u00EA ter\u00E1 que sair do seu navegador, \
6787 -  reinici\u00E1-lo, e retornar a esta p\u00E1gina web para executar a aplica\u00E7\u00E3o. \
6788 -  Quando o di\u00E1logo de seguran\u00E7a aparecer, clique no bot\u00E3o para aceitar a \
6789 -  assinatura digital e conceder a este aplicativo os privil\u00E9gios necess\u00E1rios \
6790 +m.insufficient_permissions_error = Você não aceitou a assinatura digital \
6791 +  do aplicativo. Se você quiser executar o aplicativo, você terá que aceitar \
6792 +  a assinatura digital. \n\nPara fazer isso, você terá que sair do seu navegador, \
6793 +  reiniciá-lo, e retornar a esta página web para executar a aplicação. \
6794 +  Quando o diálogo de segurança aparecer, clique no botão para aceitar a \
6795 +  assinatura digital e conceder a este aplicativo os privilégios necessários \
6796    para executar.
6797  
6798 -m.corrupt_digest_signature_error = N\u00E3o conseguimos verificar a assinatura digital \
6799 -  do aplicativo.\nPor favor, verifique se voc\u00EA est\u00E1 utilizando o aplicativo \nde um \
6800 +m.corrupt_digest_signature_error = Não conseguimos verificar a assinatura digital \
6801 +  do aplicativo.\nPor favor, verifique se você está utilizando o aplicativo \nde um \
6802    site correto.
6803  
6804 -m.default_install_error = a se\u00E7\u00E3o de suporte do site
6805 -
6806 -m.another_getdown_running = V\u00E1rias inst\u00E2ncias desta aplica\u00E7\u00E3o \
6807 -  est\u00E3o em execu\u00E7\u00E3o. Esta ir\u00E1 parar e deixar outra completar suas atividades.
6808 +m.default_install_error = a seção de suporte do site
6809  
6810 -m.applet_stopped = Foi solicitado ao miniaplicativo GetDow que parasse de trabalhar.
6811 +m.another_getdown_running = Várias instâncias desta aplicação \
6812 +  estão em execução. Esta irá parar e deixar outra completar suas atividades.
6813  
6814  # application/digest errors
6815 -m.missing_appbase = O arquivo de configura\u00E7\u00E3o n\u00E3o possui o 'AppBase'.
6816 -m.invalid_version = O arquivo de configura\u00E7\u00E3o especifica uma vers\u00E3o inv\u00E1lida.
6817 -m.invalid_appbase = O arquivo de configura\u00E7\u00E3o especifica um 'AppBase' inv\u00E1lido.
6818 -m.missing_class = O arquivo de configura\u00E7\u00E3o n\u00E3o possui a classe de aplicativo.
6819 -m.missing_code = O arquivo de configura\u00E7\u00E3o n\u00E3o especifica um recurso de c\u00F3digo.
6820 -m.invalid_digest_file = O arquivo digest \u00E9 inv\u00E1lido.
6821 +m.missing_appbase = O arquivo de configuração não possui o 'AppBase'.
6822 +m.invalid_version = O arquivo de configuração especifica uma versão inválida.
6823 +m.invalid_appbase = O arquivo de configuração especifica um 'AppBase' inválido.
6824 +m.missing_class = O arquivo de configuração não possui a classe de aplicativo.
6825 +m.missing_code = O arquivo de configuração não especifica um recurso de código.
6826 +m.invalid_digest_file = O arquivo digest é inválido.
6827 diff --git a/getdown/src/getdown/launcher/src/main/resources/com/threerings/getdown/messages_zh.properties b/getdown/src/getdown/launcher/src/main/resources/com/threerings/getdown/messages_zh.properties
6828 index 2c275437b..fa74fb293 100644
6829 --- a/getdown/src/getdown/launcher/src/main/resources/com/threerings/getdown/messages_zh.properties
6830 +++ b/getdown/src/getdown/launcher/src/main/resources/com/threerings/getdown/messages_zh.properties
6831 @@ -1,61 +1,57 @@
6832  #
6833 -# $Id$
6834 -#
6835 -# Getdown translation messages
6836 +# Getdown Chinese translation messages
6837  
6838 -m.detecting_proxy = \u641c\u5bfb\u4ee3\u7406\u670d\u52a1\u5668
6839 +m.detecting_proxy = 搜寻代理服务器
6840  
6841 -m.configure_proxy = <html>\u6211\u4eec\u65e0\u6cd5\u8fde\u63a5\u5230\u670d\u52a1\u5668\u4e0b\u8f7d\u6e38\u620f\u6570\u636e\u3002\u8fd9\u53ef\u80fd\u662f\u7531\u4e8e \
6842 -  \u60a8\u7684\u8ba1\u7b97\u673a\u662f\u901a\u8fc7\u4ee3\u7406\u670d\u52a1\u5668\u8fde\u63a5\u4e92\u8054\u7f51\u7684\uff0c\u5e76\u4e14\u6211\u4eec\u65e0\u6cd5\u81ea\u52a8\u83b7\u5f97\u4ee3\u7406\u670d\u52a1\u5668\u7684 \
6843 -  \u8bbe\u7f6e\u3002\u5982\u679c\u60a8\u77e5\u9053\u60a8\u4ee3\u7406\u670d\u52a1\u5668\u7684\u8bbe\u7f6e\uff0c\u60a8\u53ef\u4ee5\u5728\u4e0b\u9762\u8f93\u5165\u3002</html>
6844 +m.configure_proxy = <html>我们无法连接到服务器下载游戏数据。这可能是由于 \
6845 +  您的计算机是通过代理服务器连接互联网的,并且我们无法自动获得代理服务器的 \
6846 +  设置。如果您知道您代理服务器的设置,您可以在下面输入。</html>
6847  
6848 -m.proxy_extra = <html>\u5982\u679c\u60a8\u786e\u5b9a\u60a8\u6ca1\u6709\u4f7f\u7528\u4ee3\u7406\u670d\u52a1\u5668\uff0c\u8fd9\u53ef\u80fd\u662f\u7531\u4e8e\u6682\u65f6\u65e0\u6cd5 \
6849 -  \u8fde\u63a5\u5230\u4e92\u8054\u7f51\uff0c\u5bfc\u81f4\u65e0\u6cd5\u548c\u670d\u52a1\u5668\u901a\u8baf\u3002\u8fd9\u79cd\u60c5\u51b5\uff0c\u60a8\u53ef\u4ee5\u53d6\u6d88\uff0c\u7a0d\u5019\u518d\u91cd\u65b0\u5b89\u88c5\u3002<br><br> \
6850 -  \u5982\u679c\u60a8\u65e0\u6cd5\u786e\u5b9a\u60a8\u662f\u5426\u4f7f\u7528\u4e86\u4ee3\u7406\u670d\u52a1\u5668\uff0c\u8bf7\u8bbf\u95ee\u6211\u4eec\u7f51\u7ad9\u4e2d\u7684\u6280\u672f\u652f\u6301\u90e8\u4efd\uff0c \
6851 -  \u4e86\u89e3\u5982\u4f55\u68c0\u6d4b\u60a8\u7684\u4ee3\u7406\u670d\u52a1\u5668\u8bbe\u7f6e\u3002</html>
6852 +m.proxy_extra = <html>如果您确定您没有使用代理服务器,这可能是由于暂时无法 \
6853 +  连接到互联网,导致无法和服务器通讯。这种情况,您可以取消,稍候再重新安装。<br><br> \
6854 +  如果您无法确定您是否使用了代理服务器,请访问我们网站中的技术支持部份, \
6855 +  了解如何检测您的代理服务器设置。</html>
6856  
6857 -m.proxy_host = \u4ee3\u7406\u670d\u52a1\u5668\u7684IP\u5730\u5740
6858 -m.proxy_port = \u4ee3\u7406\u670d\u52a1\u5668\u7684\u7aef\u53e3\u53f7
6859 +m.proxy_host = 代理服务器的IP地址
6860 +m.proxy_port = 代理服务器的端口号
6861  m.proxy_username = Username
6862  m.proxy_password = Password
6863  m.proxy_auth_required = Authentication required
6864 -m.proxy_ok = \u786e\u5b9a
6865 -m.proxy_cancel = \u53d6\u6d88
6866 -
6867 -m.resolving = \u5206\u6790\u9700\u4e0b\u8f7d\u5185\u5bb9
6868 -m.downloading = \u4e0b\u8f7d\u6570\u636e
6869 -m.failure = \u4e0b\u8f7d\u5931\u8d25: {0}
6870 -
6871 -m.checking = \u68c0\u67e5\u66f4\u65b0\u5185\u5bb9
6872 -m.validating = \u786e\u8ba4
6873 -m.patching = \u5347\u7ea7
6874 -m.launching = \u542f\u52a8
6875 +m.proxy_ok = 确定
6876 +m.proxy_cancel = 取消
6877  
6878 -m.complete = {0}% \u5b8c\u6210
6879 -m.remain = {0} \u5269\u4f59\u65f6\u95f4
6880 +m.resolving = 分析需下载内容
6881 +m.downloading = 下载数据
6882 +m.failure = 下载失败: {0}
6883  
6884 -m.updating_metadata = \u4e0b\u8f7d\u63a7\u5236\u6587\u4ef6
6885 +m.checking = 检查更新内容
6886 +m.validating = 确认
6887 +m.patching = 升级
6888 +m.launching = 启动
6889  
6890 -m.init_failed = \u65e0\u6cd5\u627e\u5230\u914d\u7f6e\u6587\u4ef6\u6216\u5df2\u635f\u574f\u3002\u5c1d\u8bd5\u91cd\u65b0\u4e0b\u8f7d...
6891 +m.complete = {0}% 完成
6892 +m.remain = {0} 剩余时间
6893  
6894 -m.unable_to_repair = \u7ecf\u8fc75\u6b21\u5c1d\u8bd5\uff0c\u4f9d\u7136\u65e0\u6cd5\u4e0b\u8f7d\u6240\u9700\u7684\u6587\u4ef6\u3002\
6895 -\u60a8\u53ef\u4ee5\u91cd\u65b0\u8fd0\u884c\u7a0b\u5e8f\uff0c\u4f46\u662f\u5982\u679c\u4f9d\u7136\u5931\u8d25\uff0c\u60a8\u53ef\u80fd\u9700\u8981\u53cd\u5b89\u88c5\u5e76\u91cd\u65b0\u5b89\u88c5\u3002
6896 +m.updating_metadata = 下载控制文件
6897  
6898 +m.init_failed = 无法找到配置文件或已损坏。尝试重新下载...
6899  
6900 -m.unknown_error = \u7531\u4e8e\u4e00\u4e9b\u65e0\u6cd5\u56de\u590d\u7684\u4e25\u91cd\u9519\u8bef\uff0c\u7a0b\u5e8f\u542f\u52a8\u5931\u8d25\u3002\
6901 -\u8bf7\u8bbf\u95ee\u6211\u4eec\u7684\u7f51\u7ad9\u7684\u6280\u672f\u652f\u6301\u90e8\u4efd\uff0c\u4e86\u89e3\u5982\u4f55\u89e3\u51b3\u95ee\u9898\u3002
6902 +m.unable_to_repair = 经过5次尝试,依然无法下载所需的文件。\
6903 +您可以重新运行程序,但是如果依然失败,您可能需要反安装并重新安装。
6904  
6905 -m.init_error = \u7531\u4e8e\u4e0b\u5217\u9519\u8bef\uff0c\u7a0b\u5e8f\u542f\u52a8\u5931\u8d25\uff1a\n{0}\n\n \
6906 -\u8bf7\u8bbf\u95ee\u6211\u4eec\u7684\u7f51\u7ad9\u7684\u6280\u672f\u652f\u6301\u90e8\u4efd\uff0c\u4e86\u89e3\u5982\u4f55\u5904\u7406\u8fd9\u4e9b\u9519\u8bef\u3002
6907 +m.unknown_error = 由于一些无法回复的严重错误,程序启动失败。\
6908 +请访问我们的网站的技术支持部份,了解如何解决问题。
6909  
6910 +m.init_error = 由于下列错误,程序启动失败:\n{0}\n\n \
6911 +请访问我们的网站的技术支持部份,了解如何处理这些错误。
6912  
6913 -m.missing_resource = \u7531\u4e8e\u65e0\u6cd5\u627e\u5230\u4e0b\u5217\u8d44\u6e90\uff0c\u7a0b\u5e8f\u542f\u52a8\u5931\u8d25\uff1a\n{0}\n\n \
6914 -\u8bf7\u8bbf\u95ee\u6211\u4eec\u7684\u7f51\u7ad9\u7684\u6280\u672f\u652f\u6301\u90e8\u4efd\uff0c\u4e86\u89e3\u5982\u4f55\u5904\u7406\u8fd9\u4e9b\u95ee\u9898\u3002
6915 +m.missing_resource = 由于无法找到下列资源,程序启动失败:\n{0}\n\n \
6916 +请访问我们的网站的技术支持部份,了解如何处理这些问题。
6917  
6918  # application/digest errors
6919 -m.missing_appbase = \u914d\u7f6e\u6587\u4ef6\u4e2d\u65e0\u6cd5\u627e\u5230 'appbase'\u3002
6920 -m.invalid_version = \u914d\u7f6e\u6587\u4ef6\u6307\u5b9a\u4e86\u65e0\u6548\u7684\u7248\u672c\u3002
6921 -m.invalid_appbase = \u914d\u7f6e\u6587\u4ef6\u6307\u5b9a\u4e86\u65e0\u6548\u7684 'appbase'\u3002
6922 -m.missing_class = \u914d\u7f6e\u6587\u4ef6\u4e2d\u65e0\u6cd5\u627e\u5230\u7a0b\u5e8f\u6587\u4ef6\u3002
6923 -m.missing_code = \u914d\u7f6e\u6587\u4ef6\u4e2d\u65e0\u6cd5\u627e\u5230\u6307\u5b9a\u7684\u8d44\u6e90\u3002
6924 -m.invalid_digest_file = \u65e0\u6548\u7684\u914d\u7f6e\u6587\u4ef6\u3002
6925 +m.missing_appbase = 配置文件中无法找到 'appbase'。
6926 +m.invalid_version = 配置文件指定了无效的版本。
6927 +m.invalid_appbase = 配置文件指定了无效的 'appbase'。
6928 +m.missing_class = 配置文件中无法找到程序文件。
6929 +m.missing_code = 配置文件中无法找到指定的资源。
6930 +m.invalid_digest_file = 无效的配置文件。
6931 diff --git a/getdown/src/getdown/lib/commons-compress-1.18.jar b/getdown/src/getdown/lib/commons-compress-1.18.jar
6932 deleted file mode 100644
6933 index e401046b5..000000000
6934 Binary files a/getdown/src/getdown/lib/commons-compress-1.18.jar and /dev/null differ
6935 diff --git a/getdown/src/getdown/mvn_cmd b/getdown/src/getdown/mvn_cmd
6936 deleted file mode 100644
6937 index 0ce786ff8..000000000
6938 --- a/getdown/src/getdown/mvn_cmd
6939 +++ /dev/null
6940 @@ -1 +0,0 @@
6941 -mvn clean package -Dgetdown.host.whitelist="jalview.org,*.jalview.org" && cp launcher/target/getdown-launcher-1.8.3-SNAPSHOT.jar ../../../getdown/lib/getdown-launcher.jar && cp core/target/getdown-core-1.8.3-SNAPSHOT.jar ../../../getdown/lib/getdown-core-1.8.3-SNAPSHOT.jar && cp core/target/getdown-core-1.8.3-SNAPSHOT.jar ../../../j8lib/getdown-core.jar && cp core/target/getdown-core-1.8.3-SNAPSHOT.jar ../../../j11lib/getdown-core.jar 
6942 diff --git a/getdown/src/getdown/pom.xml b/getdown/src/getdown/pom.xml
6943 index 78d67b0a5..ae1370dde 100644
6944 --- a/getdown/src/getdown/pom.xml
6945 +++ b/getdown/src/getdown/pom.xml
6946 @@ -10,7 +10,7 @@
6947    <groupId>com.threerings.getdown</groupId>
6948    <artifactId>getdown</artifactId>
6949    <packaging>pom</packaging>
6950 -  <version>1.8.3-SNAPSHOT</version>
6951 +  <version>1.8.7-SNAPSHOT</version>
6952  
6953    <name>getdown</name>
6954    <description>An application installer and updater.</description>
6955 @@ -35,6 +35,10 @@
6956      </developer>
6957    </developers>
6958  
6959 +  <properties>
6960 +    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
6961 +  </properties>
6962 +
6963    <scm>
6964      <connection>scm:git:git://github.com/threerings/getdown.git</connection>
6965      <developerConnection>scm:git:git@github.com:threerings/getdown.git</developerConnection>
6966 @@ -47,28 +51,6 @@
6967      <module>ant</module>
6968    </modules>
6969  
6970 -  <repositories>
6971 -       <repository>
6972 -          <id>ej-technologies</id>
6973 -          <url>https://maven.ej-technologies.com/repository</url>
6974 -       </repository>
6975 -  </repositories>
6976 -
6977 -
6978 -  <dependencies>
6979 -    <dependency>
6980 -      <groupId>org.apache.commons</groupId>
6981 -      <artifactId>commons-compress</artifactId>
6982 -      <version>1.18</version>
6983 -    </dependency>
6984 -    <dependency>
6985 -               <groupId>com.install4j</groupId>
6986 -               <artifactId>install4j-runtime</artifactId>
6987 -               <version>7.0.11</version>
6988 -               <scope>provided</scope>
6989 -       </dependency>
6990 -  </dependencies>
6991 -
6992    <build>
6993      <plugins>
6994        <plugin>
6995 @@ -83,6 +65,20 @@
6996            <stagingProfileId>aa555c46fc37d0</stagingProfileId>
6997          </configuration>
6998        </plugin>
6999 +
7000 +      <plugin>
7001 +        <groupId>org.apache.maven.plugins</groupId>
7002 +        <artifactId>maven-javadoc-plugin</artifactId>
7003 +        <version>3.1.0</version>
7004 +        <configuration>
7005 +          <quiet>true</quiet>
7006 +          <show>public</show>
7007 +          <additionalOptions>
7008 +            <additionalOption>-Xdoclint:all</additionalOption>
7009 +            <additionalOption>-Xdoclint:-missing</additionalOption>
7010 +          </additionalOptions>
7011 +        </configuration>
7012 +      </plugin>
7013      </plugins>
7014  
7015      <!-- Common plugin configuration for all children -->
7016 @@ -91,10 +87,10 @@
7017          <plugin>
7018            <groupId>org.apache.maven.plugins</groupId>
7019            <artifactId>maven-compiler-plugin</artifactId>
7020 -          <version>3.8.1</version>
7021 +          <version>3.7.0</version>
7022            <configuration>
7023 -            <source>1.8</source>
7024 -            <target>1.8</target>
7025 +            <source>1.7</source>
7026 +            <target>1.7</target>
7027              <fork>true</fork>
7028              <showDeprecation>true</showDeprecation>
7029              <showWarnings>true</showWarnings>
7030 @@ -110,20 +106,12 @@
7031            <groupId>org.apache.maven.plugins</groupId>
7032            <artifactId>maven-resources-plugin</artifactId>
7033            <version>3.0.2</version>
7034 -          <configuration>
7035 -            <encoding>UTF-8</encoding>
7036 -          </configuration>
7037          </plugin>
7038  
7039          <plugin>
7040            <groupId>org.apache.maven.plugins</groupId>
7041 -          <artifactId>maven-javadoc-plugin</artifactId>
7042 -          <version>3.0.0-M1</version>
7043 -          <configuration>
7044 -            <quiet>true</quiet>
7045 -            <show>public</show>
7046 -            <additionalparam>-Xdoclint:all -Xdoclint:-missing</additionalparam>
7047 -          </configuration>
7048 +          <artifactId>maven-gpg-plugin</artifactId>
7049 +          <version>1.6</version>
7050          </plugin>
7051        </plugins>
7052      </pluginManagement>
7053 @@ -181,7 +169,6 @@
7054            <plugin>
7055              <groupId>org.apache.maven.plugins</groupId>
7056              <artifactId>maven-gpg-plugin</artifactId>
7057 -            <version>1.6</version>
7058              <executions>
7059                <execution>
7060                  <id>sign-artifacts</id>