29 November 2015
+
Firecage updated:
+
+ - Hand tools now have variable operating speeds.
+
+
GunHog updated:
+
+ - Nanotrasen has improved the interface for the hand-held teleportation device. It will now provide the user with the name of the destination tracking beacon.
+
+
JJRcop updated:
+
+ - Fixes changeling Hive Absorb DNA ability runtime.
+
+
KorPhaeron updated:
+
+ - Guardians and holoparasites can now be manually recalled by their master.
+ - The ghost controlling a guardian or holoparasite can be repicked by their master, only one use which is removed when a new ghost is chosen.
+
MrStonedOne updated:
- Re-adds 100% chance timed injectors, as it was removed over a misunderstanding.
- Fixes bug with injectors having a 100% power chance despite not being timed
+ - Fixes the excessive amount of time it took the game to initialize.
+ - Fixes some interface bugs.
+ - when an interface window has to send a lot of resources before it can open, it will now tell you with a "sending resources" line so you know why it's not opening right away.
PKPenguin321 updated:
@@ -67,6 +87,29 @@
- Teleprods can have their crystal removed and be made back into stunprods by using them in your hand when they have no cell installed.
- Mounted chainsaws now have a unique inhand sprite.
+
Remie + Kor updated:
+
+ - Holopads now use AltClick to request the AI
+
+
TheNightingale updated:
+
+ - Nuclear operatives can now buy the Elite Syndicate Hardsuit for 8 TC that has better armor and fireproofing.
+ - Added throwing stars and Syndicate playing cards to nuclear operative's uplink.
+
+
Yolopanther12 updated:
+
+ - Added the ability to use a box of lights on a light replacer to more quickly refill it to capacity.
+
+
incoming5643 updated:
+
+ - OOC will now take steps to try and prevent accidental IC from people who mistakenly use OOC instead of say. This doesn't catch all mistakes, so please don't test it.
+
+
ktccd updated:
+
+ - Blob pieces on some areas no longer count for blob mode victory (Such as space or asteroid).
+ - Blobcode no longer spawns and destroys blobs uselessly, which fixes other stuff too.
+ - Asteroid areas are no longer valid gang territory.
+
neersighted updated:
- Nanotrasen would like to announce NanoUI 2.0, now being rolled out across all our stations. Existing NanoUI devices have been updated, and more will be added soon. Please report any bugs to Nanotrasen Support.
@@ -74,6 +117,11 @@
- Some NanoUI interfaces have had more information added.
- Drones can now interact with NanoUI windows.
+
swankcookie updated:
+
+ - Fixes issue of space-Christians forgetting about the true, humbling values of space-Christmas.
+ - Adds snowmen outfits
+
28 November 2015
TechnoAlchemisto updated:
diff --git a/html/changelogs/.all_changelog.yml b/html/changelogs/.all_changelog.yml
index 56d72683ca9..6abd4e726ed 100644
--- a/html/changelogs/.all_changelog.yml
+++ b/html/changelogs/.all_changelog.yml
@@ -2604,10 +2604,26 @@ DO NOT EDIT THIS FILE BY HAND! AUTOMATICALLY GENERATED BY ss13_genchangelog.py.
TechnoAlchemisto:
- tweak: Cloaks are now worn in your exosuit slot!
2015-11-29:
+ Firecage:
+ - rscadd: Hand tools now have variable operating speeds.
+ GunHog:
+ - tweak: Nanotrasen has improved the interface for the hand-held teleportation device.
+ It will now provide the user with the name of the destination tracking beacon.
+ JJRcop:
+ - bugfix: Fixes changeling Hive Absorb DNA ability runtime.
+ KorPhaeron:
+ - rscadd: Guardians and holoparasites can now be manually recalled by their master.
+ - rscadd: The ghost controlling a guardian or holoparasite can be repicked by their
+ master, only one use which is removed when a new ghost is chosen.
MrStonedOne:
- rscadd: Re-adds 100% chance timed injectors, as it was removed over a misunderstanding.
- bugfix: Fixes bug with injectors having a 100% power chance despite not being
timed
+ - bugfix: Fixes the excessive amount of time it took the game to initialize.
+ - bugfix: Fixes some interface bugs.
+ - rscadd: when an interface window has to send a lot of resources before it can
+ open, it will now tell you with a "sending resources" line so you know why it's
+ not opening right away.
PKPenguin321:
- rscadd: Teleprods can now be constructed. Make them via tablecrafting, or by putting
a bluespace crystal onto a stunprod. They warp the target to a random nearby
@@ -2616,6 +2632,26 @@ DO NOT EDIT THIS FILE BY HAND! AUTOMATICALLY GENERATED BY ss13_genchangelog.py.
- rscadd: Teleprods can have their crystal removed and be made back into stunprods
by using them in your hand when they have no cell installed.
- imageadd: Mounted chainsaws now have a unique inhand sprite.
+ Remie + Kor:
+ - tweak: Holopads now use AltClick to request the AI
+ TheNightingale:
+ - rscadd: Nuclear operatives can now buy the Elite Syndicate Hardsuit for 8 TC that
+ has better armor and fireproofing.
+ - tweak: Added throwing stars and Syndicate playing cards to nuclear operative's
+ uplink.
+ Yolopanther12:
+ - rscadd: Added the ability to use a box of lights on a light replacer to more quickly
+ refill it to capacity.
+ incoming5643:
+ - rscadd: OOC will now take steps to try and prevent accidental IC from people who
+ mistakenly use OOC instead of say. This doesn't catch all mistakes, so please
+ don't test it.
+ ktccd:
+ - bugfix: Blob pieces on some areas no longer count for blob mode victory (Such
+ as space or asteroid).
+ - bugfix: Blobcode no longer spawns and destroys blobs uselessly, which fixes other
+ stuff too.
+ - bugfix: Asteroid areas are no longer valid gang territory.
neersighted:
- experiment: Nanotrasen would like to announce NanoUI 2.0, now being rolled out
across all our stations. Existing NanoUI devices have been updated, and more
@@ -2623,3 +2659,7 @@ DO NOT EDIT THIS FILE BY HAND! AUTOMATICALLY GENERATED BY ss13_genchangelog.py.
- tweak: Many NanoUI interfaces have had +/- buttons replaced with input fields.
- tweak: Some NanoUI interfaces have had more information added.
- bugfix: Drones can now interact with NanoUI windows.
+ swankcookie:
+ - bugfix: Fixes issue of space-Christians forgetting about the true, humbling values
+ of space-Christmas.
+ - rscadd: Adds snowmen outfits
diff --git a/icons/mob/animal.dmi b/icons/mob/animal.dmi
index 93856d3e80d..b1c94f4780c 100644
Binary files a/icons/mob/animal.dmi and b/icons/mob/animal.dmi differ
diff --git a/icons/mob/head.dmi b/icons/mob/head.dmi
index 009bba1db5c..c58a95408ab 100644
Binary files a/icons/mob/head.dmi and b/icons/mob/head.dmi differ
diff --git a/icons/mob/suit.dmi b/icons/mob/suit.dmi
index 847f3d4302c..ef3d349f6c8 100644
Binary files a/icons/mob/suit.dmi and b/icons/mob/suit.dmi differ
diff --git a/icons/obj/clothing/hats.dmi b/icons/obj/clothing/hats.dmi
index 4f6d6740f67..d56674bce74 100644
Binary files a/icons/obj/clothing/hats.dmi and b/icons/obj/clothing/hats.dmi differ
diff --git a/icons/obj/clothing/suits.dmi b/icons/obj/clothing/suits.dmi
index 82e89958418..9941fef084c 100644
Binary files a/icons/obj/clothing/suits.dmi and b/icons/obj/clothing/suits.dmi differ
diff --git a/nano/images/icon-eye.xcf b/nano/image_source/icon-eye.xcf
similarity index 100%
rename from nano/images/icon-eye.xcf
rename to nano/image_source/icon-eye.xcf
diff --git a/nano/images/source/NTLogoRevised.fla b/nano/image_source/source/NTLogoRevised.fla
similarity index 100%
rename from nano/images/source/NTLogoRevised.fla
rename to nano/image_source/source/NTLogoRevised.fla
diff --git a/nano/images/source/icon-eye.xcf b/nano/image_source/source/icon-eye.xcf
similarity index 100%
rename from nano/images/source/icon-eye.xcf
rename to nano/image_source/source/icon-eye.xcf
diff --git a/nano/images/source/uiBackground-Syndicate.xcf b/nano/image_source/source/uiBackground-Syndicate.xcf
similarity index 100%
rename from nano/images/source/uiBackground-Syndicate.xcf
rename to nano/image_source/source/uiBackground-Syndicate.xcf
diff --git a/nano/images/source/uiBackground.fla b/nano/image_source/source/uiBackground.fla
similarity index 100%
rename from nano/images/source/uiBackground.fla
rename to nano/image_source/source/uiBackground.fla
diff --git a/nano/images/source/uiBackground.xcf b/nano/image_source/source/uiBackground.xcf
similarity index 100%
rename from nano/images/source/uiBackground.xcf
rename to nano/image_source/source/uiBackground.xcf
diff --git a/nano/images/source/uiBasicBackground.xcf b/nano/image_source/source/uiBasicBackground.xcf
similarity index 100%
rename from nano/images/source/uiBasicBackground.xcf
rename to nano/image_source/source/uiBasicBackground.xcf
diff --git a/nano/images/source/uiIcons16Green.xcf b/nano/image_source/source/uiIcons16Green.xcf
similarity index 100%
rename from nano/images/source/uiIcons16Green.xcf
rename to nano/image_source/source/uiIcons16Green.xcf
diff --git a/nano/images/source/uiIcons16Red.xcf b/nano/image_source/source/uiIcons16Red.xcf
similarity index 100%
rename from nano/images/source/uiIcons16Red.xcf
rename to nano/image_source/source/uiIcons16Red.xcf
diff --git a/nano/images/source/uiIcons24.xcf b/nano/image_source/source/uiIcons24.xcf
similarity index 100%
rename from nano/images/source/uiIcons24.xcf
rename to nano/image_source/source/uiIcons24.xcf
diff --git a/nano/images/source/uiNoticeBackground.xcf b/nano/image_source/source/uiNoticeBackground.xcf
similarity index 100%
rename from nano/images/source/uiNoticeBackground.xcf
rename to nano/image_source/source/uiNoticeBackground.xcf
diff --git a/nano/images/source/uiTitleBackground.xcf b/nano/image_source/source/uiTitleBackground.xcf
similarity index 100%
rename from nano/images/source/uiTitleBackground.xcf
rename to nano/image_source/source/uiTitleBackground.xcf
diff --git a/nano/images/uiBackground.xcf b/nano/image_source/uiBackground.xcf
similarity index 100%
rename from nano/images/uiBackground.xcf
rename to nano/image_source/uiBackground.xcf
diff --git a/nano/images/uiIcons24.xcf b/nano/image_source/uiIcons24.xcf
similarity index 100%
rename from nano/images/uiIcons24.xcf
rename to nano/image_source/uiIcons24.xcf
diff --git a/nano/images/uiNoticeBackground.xcf b/nano/image_source/uiNoticeBackground.xcf
similarity index 100%
rename from nano/images/uiNoticeBackground.xcf
rename to nano/image_source/uiNoticeBackground.xcf
diff --git a/nano/images/uiTitleBackground.xcf b/nano/image_source/uiTitleBackground.xcf
similarity index 100%
rename from nano/images/uiTitleBackground.xcf
rename to nano/image_source/uiTitleBackground.xcf
diff --git a/tgstation.dme b/tgstation.dme
index eae34d028c4..b7d5c907c8f 100644
--- a/tgstation.dme
+++ b/tgstation.dme
@@ -842,7 +842,6 @@
#include "code\modules\admin\holder2.dm"
#include "code\modules\admin\IsBanned.dm"
#include "code\modules\admin\NewBan.dm"
-#include "code\modules\admin\newbanjob.dm"
#include "code\modules\admin\player_panel.dm"
#include "code\modules\admin\secrets.dm"
#include "code\modules\admin\sql_notes.dm"
@@ -1329,6 +1328,7 @@
#include "code\modules\mob\living\simple_animal\hostile\statue.dm"
#include "code\modules\mob\living\simple_animal\hostile\syndicate.dm"
#include "code\modules\mob\living\simple_animal\hostile\tree.dm"
+#include "code\modules\mob\living\simple_animal\hostile\wizard.dm"
#include "code\modules\mob\living\simple_animal\hostile\zombie.dm"
#include "code\modules\mob\living\simple_animal\hostile\retaliate\bat.dm"
#include "code\modules\mob\living\simple_animal\hostile\retaliate\clown.dm"
diff --git a/tools/dmitool/README.txt b/tools/dmitool/README.txt
new file mode 100644
index 00000000000..409ab873c3d
--- /dev/null
+++ b/tools/dmitool/README.txt
@@ -0,0 +1,5 @@
+Uses PNGJ: https://code.google.com/p/pngj/.
+
+For help, use "java -jar dmitool.jar help".
+
+Requires Java 7.
\ No newline at end of file
diff --git a/tools/dmitool/build.gradle b/tools/dmitool/build.gradle
new file mode 100644
index 00000000000..2e5fbc5b08e
--- /dev/null
+++ b/tools/dmitool/build.gradle
@@ -0,0 +1,16 @@
+apply plugin: 'java'
+
+repositories {
+ mavenCentral()
+}
+
+dependencies {
+ compile group: 'ar.com.hjg', name: 'pngj', version: '2.1.0'
+}
+
+jar {
+ from { configurations.compile.collect { it.isDirectory() ? it : zipTree(it) } }
+ manifest {
+ attributes 'Main-Class': 'dmitool.Main'
+ }
+}
\ No newline at end of file
diff --git a/tools/dmitool/dmimerge.sh b/tools/dmitool/dmimerge.sh
new file mode 100644
index 00000000000..dbe846fc6a9
--- /dev/null
+++ b/tools/dmitool/dmimerge.sh
@@ -0,0 +1,8 @@
+java -jar tools/dmitool/dmitool.jar merge $1 $2 $3 $2
+if [ "$?" -gt 0 ]
+then
+ echo "Unable to automatically resolve all icon_state conflicts, please merge manually."
+ exit 1
+fi
+
+exit 0
diff --git a/tools/dmitool/dmitool.jar b/tools/dmitool/dmitool.jar
new file mode 100644
index 00000000000..4cfa49f36de
Binary files /dev/null and b/tools/dmitool/dmitool.jar differ
diff --git a/tools/dmitool/dmitool.py b/tools/dmitool/dmitool.py
new file mode 100644
index 00000000000..390f0d745ff
--- /dev/null
+++ b/tools/dmitool/dmitool.py
@@ -0,0 +1,94 @@
+""" Python 2.7 wrapper for dmitool.
+"""
+
+import os
+from subprocess import Popen, PIPE
+
+_JAVA_PATH = ["java"]
+_DMITOOL_CMD = ["-jar", "dmitool.jar"]
+
+def _dmitool_call(*dmitool_args, **popen_args):
+ return Popen(_JAVA_PATH + _DMITOOL_CMD + [str(arg) for arg in dmitool_args], **popen_args)
+
+def _safe_parse(dict, key, deferred_value):
+ try:
+ dict[key] = deferred_value()
+ except Exception as e:
+ print "Could not parse property '%s': %s"%(key, e)
+ return e
+ return False
+
+def version():
+ """ Returns the version as a string. """
+ stdout, stderr = _dmitool_call("version", stdout=PIPE).communicate()
+ return str(stdout).strip()
+
+def help():
+ """ Returns the help text as a string. """
+ stdout, stderr = _dmitool_call("help", stdout=PIPE).communicate()
+ return str(stdout).strip()
+
+def info(filepath):
+ """ Totally not a hack that parses the output from dmitool into a dictionary.
+ May break at any moment.
+ """
+ subproc = _dmitool_call("info", filepath, stdout=PIPE)
+ stdout, stderr = subproc.communicate()
+
+ result = {}
+ data = stdout.split(os.linesep)[1:]
+ #for s in data: print s
+
+ #parse header line
+ if len(data) > 0:
+ header = data.pop(0).split(",")
+ #don't need to parse states, it's redundant
+ _safe_parse(result, "images", lambda: int(header[0].split()[0].strip()))
+ _safe_parse(result, "size", lambda: header[2].split()[1].strip())
+
+ #parse state information
+ states = []
+ for item in data:
+ if not len(item): continue
+
+ stateinfo = {}
+ item = item.split(",", 3)
+ _safe_parse(stateinfo, "name", lambda: item[0].split()[1].strip(" \""))
+ _safe_parse(stateinfo, "dirs", lambda: int(item[1].split()[0].strip()))
+ _safe_parse(stateinfo, "frames", lambda: int(item[2].split()[0].strip()))
+ if len(item) > 3:
+ stateinfo["misc"] = item[3]
+
+ states.append(stateinfo)
+
+ result["states"] = states
+ return result
+
+def extract_state(input_path, output_path, icon_state, direction=None, frame=None):
+ """ Extracts an icon state as a png to a given path.
+ If provided direction should be a string, one of S, N, E, W, SE, SW, NE, NW.
+ If provided frame should be a frame number or a string of two frame number separated by a dash.
+ """
+ args = ["extract", input_path, icon_state, output_path]
+ if direction is not None: args.extend(("direction" , str(direction)))
+ if frame is not None: args.extend(("frame" , str(frame)))
+ return _dmitool_call(*args)
+
+def import_state(target_path, input_path, icon_state, replace=False, delays=None, rewind=False, loop=None, ismovement=False, direction=None, frame=None):
+ """ Inserts an input png given by the input_path into the target_path.
+ """
+ args = ["import", target_path, icon_state, input_path]
+
+ if replace: args.append("nodup")
+ if rewind: args.append("rewind")
+ if ismovement: args.append("movement")
+ if delays: args.extend(("delays", ",".join(delays)))
+ if direction is not None: args.extend(("direction", direction))
+ if frame is not None: args.extend(("frame", frame))
+
+ if loop in ("inf", "infinity"):
+ args.append("loop")
+ elif loop:
+ args.extend(("loopn", loop))
+
+ return _dmitool_call(*args)
diff --git a/tools/dmitool/git_merge_installer.bat b/tools/dmitool/git_merge_installer.bat
new file mode 100644
index 00000000000..18ab7c8026a
--- /dev/null
+++ b/tools/dmitool/git_merge_installer.bat
@@ -0,0 +1,6 @@
+@echo off
+set tab=
+echo. >> ../../.git/config
+echo [merge "merge-dmi"] >> ../../.git/config
+echo %tab%name = iconfile merge driver >> ../../.git/config
+echo %tab%driver = ./tools/dmitool/dmimerge.sh %%O %%A %%B >> ../../.git/config
diff --git a/tools/dmitool/git_merge_installer.sh b/tools/dmitool/git_merge_installer.sh
new file mode 100755
index 00000000000..e0c48823b11
--- /dev/null
+++ b/tools/dmitool/git_merge_installer.sh
@@ -0,0 +1,6 @@
+F="../../.git/config"
+
+echo '' >> $F
+echo '[merge "merge-dmi"]' >> $F
+echo ' name = iconfile merge driver' >> $F
+echo ' driver = ./tools/dmitool/dmimerge.sh %O %A %B' >> $F
diff --git a/tools/dmitool/merging.txt b/tools/dmitool/merging.txt
new file mode 100644
index 00000000000..d8943efb985
--- /dev/null
+++ b/tools/dmitool/merging.txt
@@ -0,0 +1,14 @@
+1. Install java(http://www.java.com/en/download/index.jsp)
+2. Make sure java is in your PATH. To test this, open git bash, and type "java". If it says unknown command, you need to add JAVA/bin to your PATH variable (A guide for this can be found at https://www.java.com/en/download/help/path.xml ).
+
+Merging
+The easiest way to do merging is to install the merge driver. For this, open `-tg-station/.git/config` in a text editor, and paste the following lines to the end of it:
+
+[merge "merge-dmi"]
+ name = iconfile merge driver
+ driver = ./tools/dmitool/dmimerge.sh %O %A %B
+
+You may optionally instead run git_merge_installer.bat or git_merge_installer.sh which should automatically insert these lines for you at the appropriate location.
+
+After this, merging DMI files should happen automagically unless there are conflicts (an icon_state that both you and someone else changed).
+If there are conflicts, you will unfortunately still be stuck with opening both versions in the editor, and manually resolving the issues with those states.
diff --git a/tools/dmitool/src/main/java/dmitool/DMI.java b/tools/dmitool/src/main/java/dmitool/DMI.java
new file mode 100644
index 00000000000..bb560107ae9
--- /dev/null
+++ b/tools/dmitool/src/main/java/dmitool/DMI.java
@@ -0,0 +1,455 @@
+package dmitool;
+
+import ar.com.hjg.pngj.ImageInfo;
+import ar.com.hjg.pngj.ImageLineInt;
+import ar.com.hjg.pngj.PngReader;
+import ar.com.hjg.pngj.PngWriter;
+import ar.com.hjg.pngj.PngjInputException;
+import ar.com.hjg.pngj.chunks.PngChunkPLTE;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.Deque;
+import java.util.HashMap;
+import java.util.List;
+
+public class DMI implements Comparator
{
+ int w, h;
+ List images;
+ int totalImages = 0;
+ RGBA[] palette;
+ boolean isPaletted;
+
+ public DMI(int w, int h) {
+ this.w = w;
+ this.h = h;
+ images = new ArrayList<>();
+ isPaletted = false;
+ palette = null;
+ }
+
+ public DMI(String f) throws DMIException, FileNotFoundException {
+ this(new File(f));
+ }
+
+ public DMI(File f) throws DMIException, FileNotFoundException {
+ if(f.length() == 0) { // Empty .dmi is empty file
+ w = 32;
+ h = 32;
+ images = new ArrayList<>();
+ isPaletted = false;
+ palette = null;
+ return;
+ }
+ InputStream in = new FileInputStream(f);
+ PngReader pngr;
+ try {
+ pngr = new PngReader(in);
+ } catch(PngjInputException pie) {
+ throw new DMIException("Bad file format!", pie);
+ }
+ String descriptor = pngr.getMetadata().getTxtForKey("Description");
+ String[] lines = descriptor.split("\n");
+
+ if(Main.VERBOSITY > 0) System.out.println("Descriptor has " + lines.length + " lines.");
+ if(Main.VERBOSITY > 3) {
+ System.out.println("Descriptor:");
+ System.out.println(descriptor);
+ }
+
+ /* length 6 is:
+ # BEGIN DMI
+ version = 4.0
+ state = "state"
+ dirs = 1
+ frames = 1
+ # END DMI
+ */
+ if(lines.length < 6) throw new DMIException(null, 0, "Descriptor too short!");
+
+ if(!"# BEGIN DMI".equals(lines[0])) throw new DMIException(lines, 0, "Expected '# BEGIN DMI'");
+ if(!"# END DMI".equals(lines[lines.length-1])) throw new DMIException(lines, lines.length-1, "Expected '# END DMI'");
+ if(!"version = 4.0".equals(lines[1])) throw new DMIException(lines, 1, "Unknown version, expected 'version = 4.0'");
+
+ this.w = 32;
+ this.h = 32;
+
+ int i = 2;
+
+ if(lines[i].startsWith("\twidth = ")) {
+ this.w = Integer.parseInt(lines[2].substring("\twidth = ".length()));
+ i++;
+ }
+ if(lines[i].startsWith("\theight = ")) {
+ this.h = Integer.parseInt(lines[3].substring("\theight = ".length()));
+ i++;
+ }
+
+ List states = new ArrayList<>();
+
+ while(i < lines.length - 1) {
+ long imagesInState = 1;
+ if(!lines[i].startsWith("state = \"") || !lines[i].endsWith("\"")) throw new DMIException(lines, i, "Error reading state string");
+ String stateName = lines[i].substring("state = \"".length(), lines[i].length()-1);
+ i++;
+ int dirs = 1;
+ int frames = 1;
+ float[] delays = null;
+ boolean rewind = false;
+ int loop = -1;
+ String hotspot = null;
+ boolean movement = false;
+ while(lines[i].startsWith("\t")) {
+ if(lines[i].startsWith("\tdirs = ")) {
+ dirs = Integer.parseInt(lines[i].substring("\tdirs = ".length()));
+ imagesInState *= dirs;
+ i++;
+ } else if(lines[i].startsWith("\tframes = ")) {
+ frames = Integer.parseInt(lines[i].substring("\tframes = ".length()));
+ imagesInState *= frames;
+ i++;
+ } else if(lines[i].startsWith("\tdelay = ")) {
+ String delayString = lines[i].substring("\tdelay = ".length());
+ String[] delayVals = delayString.split(",");
+ delays = new float[delayVals.length];
+ for(int d=0; d 0) System.out.println(pal.getNentries() + " palette entries");
+
+ palette = new RGBA[pal.getNentries()];
+ int[] rgb = new int[3];
+ for(int q=0; q 0) System.out.println("Non-paletted image");
+ }
+
+ int iw = pngr.imgInfo.cols;
+ int ih = pngr.imgInfo.rows;
+
+ if(totalImages > iw * ih)
+ throw new DMIException(null, 0, "Impossible number of images!");
+
+ if(Main.VERBOSITY > 0) System.out.println("Image size " + iw+"x"+ih);
+ int[][] px = new int[ih][];
+
+ for(int y=0; y statesY)
+ // this should NEVER happen, we pre-check it
+ throw new DMIException(null, 0, "CRITICAL: End of image reached with states to go!");
+ }
+ }
+ if(is.delays != null) {
+ if((Main.STRICT && is.delays.length*is.dirs != img.length) || is.delays.length*is.dirs < img.length)
+ throw new DMIException(null, 0, "Delay array size mismatch: " + is.delays.length*is.dirs + " vs " + img.length + "!");
+ }
+ is.images = img;
+ }
+ }
+
+ public IconState getIconState(String name) {
+ for(IconState is: images) {
+ if(is.name.equals(name)) {
+ return is;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Makes a copy, unless name is null.
+ */
+ public void addIconState(String name, IconState is) {
+ if(name == null) {
+ images.add(is);
+ totalImages += is.dirs * is.frames;
+ } else {
+ IconState newState = (IconState)is.clone();
+ newState.name = name;
+ images.add(newState);
+ totalImages += is.dirs * is.frames;
+ }
+ }
+
+ public boolean removeIconState(String name) {
+ for(IconState is: images) {
+ if(is.name.equals(name)) {
+ images.remove(is);
+ totalImages -= is.dirs * is.frames;
+ return true;
+ }
+ }
+ return false;
+ }
+
+ public boolean setIconState(IconState is) {
+ for(int i=0; i 0) System.out.println("Fixing PNG chunks...");
+ out.writeInt(in.readInt());
+ out.writeInt(in.readInt());
+
+ Deque notZTXT = new ArrayDeque<>();
+
+ PNGChunk c = null;
+
+ while(c == null || c.type != IEND) {
+ c = new PNGChunk(in);
+ if(c.type == zTXt && notZTXT != null) {
+ PNGChunk cc = null;
+ while(cc == null || cc.type != IHDR) {
+ cc = notZTXT.pop();
+ cc.write(out);
+ }
+ c.write(out);
+ while(notZTXT.size() != 0) {
+ PNGChunk pc = notZTXT.pop();
+ pc.write(out);
+ }
+ notZTXT = null;
+ } else if(notZTXT != null) {
+ notZTXT.add(c);
+ } else {
+ c.write(out);
+ }
+ }
+ if(Main.VERBOSITY > 0) System.out.println("Chunks fixed.");
+ }
+
+ @Override public int compare(IconState arg0, IconState arg1) {
+ return arg0.name.compareTo(arg1.name);
+ }
+
+ public void writeDMI(OutputStream os) throws IOException {
+ writeDMI(os, false);
+ }
+ public void writeDMI(OutputStream os, boolean sortStates) throws IOException {
+ if(totalImages == 0) { // Empty .dmis are empty files
+ os.close();
+ return;
+ }
+
+ // Setup chunk-fix buffer
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+
+ if(sortStates) {
+ Collections.sort(images, this);
+ }
+
+ // Write the dmi into the buffer
+ int sx = (int)Math.ceil(Math.sqrt(totalImages));
+ int sy = totalImages / sx;
+ if(sx*sy < totalImages) {
+ sy++;
+ }
+ if(Main.VERBOSITY > 0) System.out.println("Image size: " + w + "x" + h + "; number of images " + sx + "x" + sy + " (" + totalImages + ")");
+ int ix = sx * w;
+ int iy = sy * h;
+ ImageInfo ii = new ImageInfo(ix, iy, 8, true);
+ PngWriter out = new PngWriter(baos, ii);
+ out.setCompLevel(9); // Maximum compression
+ String description = getDescriptor();
+ if(Main.VERBOSITY > 0) System.out.println("Descriptor has " + (description.split("\n").length) + " lines.");
+ out.getMetadata().setText("Description", description, true, true);
+
+ Image[][] img = new Image[sx][sy];
+ {
+ int k = 0;
+ int r = 0;
+ for(IconState is: images) {
+ for(Image i: is.images) {
+ img[k++][r] = i;
+
+ if(k == sx) {
+ k = 0;
+ r++;
+ }
+ }
+ }
+ }
+
+ for(int irow=0; irow myIS = new HashMap<>();
+ HashMap dmiIS = new HashMap<>();
+
+ for(IconState is: images) {
+ myIS.put(is.name, is);
+ }
+ for(IconState is: dmi.images) {
+ dmiIS.put(is.name, is);
+ }
+ if(!myIS.keySet().equals(dmiIS.keySet())) return false;
+ for(String s: myIS.keySet()) {
+ if(!myIS.get(s).equals(dmiIS.get(s))) return false;
+ }
+
+ return true;
+ }
+}
diff --git a/tools/dmitool/src/main/java/dmitool/DMIDiff.java b/tools/dmitool/src/main/java/dmitool/DMIDiff.java
new file mode 100644
index 00000000000..431d9c1d15a
--- /dev/null
+++ b/tools/dmitool/src/main/java/dmitool/DMIDiff.java
@@ -0,0 +1,194 @@
+package dmitool;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+public class DMIDiff {
+ Map newIconStates;
+ Map modifiedIconStates = new HashMap<>();
+ Set removedIconStates;
+
+ DMIDiff() {
+ newIconStates = new HashMap<>();
+ removedIconStates = new HashSet<>();
+ }
+
+ public DMIDiff(DMI base, DMI mod) {
+ if(base.h != mod.h || base.w != mod.w) throw new IllegalArgumentException("Cannot compare non-identically-sized DMIs!");
+
+ HashMap baseIS = new HashMap<>();
+ for(IconState is: base.images) {
+ baseIS.put(is.name, is);
+ }
+
+ HashMap modIS = new HashMap<>();
+ for(IconState is: mod.images) {
+ modIS.put(is.name, is);
+ }
+
+ newIconStates = ((HashMap)modIS.clone());
+ for(String s: baseIS.keySet()) {
+ newIconStates.remove(s);
+ }
+
+ removedIconStates = new HashSet<>();
+ removedIconStates.addAll(baseIS.keySet());
+ removedIconStates.removeAll(modIS.keySet());
+
+ Set retainedStates = new HashSet<>();
+ retainedStates.addAll(baseIS.keySet());
+ retainedStates.retainAll(modIS.keySet());
+
+ for(String s: retainedStates) {
+ if(!baseIS.get(s).equals(modIS.get(s))) {
+ modifiedIconStates.put(s, new IconStateDiff(baseIS.get(s), modIS.get(s)));
+ }
+ }
+ }
+ /**
+ * ASSUMES NO MERGE CONFLICTS - MERGE DIFFS FIRST.
+ */
+ public void applyToDMI(DMI dmi) {
+ for(String s: removedIconStates) {
+ dmi.removeIconState(s);
+ }
+ for(String s: modifiedIconStates.keySet()) {
+ dmi.setIconState(modifiedIconStates.get(s).newState);
+ }
+ for(String s: newIconStates.keySet()) {
+ dmi.addIconState(null, newIconStates.get(s));
+ }
+ }
+
+ /**
+ * @param other The diff to merge with
+ * @param conflictDMI A DMI to add conflicted icon_states to
+ * @param merged An empty DMIDiff to merge into
+ * @param aName The log name for this diff
+ * @param bName The log name for {@code other}
+ * @return A Set containing all icon_states which conflicted, along with what was done in each diff, in the format "icon_state: here|there"; here and there are one of "added", "modified", and "removed"
+ */
+ public Set mergeDiff(DMIDiff other, DMI conflictDMI, DMIDiff merged, String aName, String bName) {
+ HashSet myTouched = new HashSet<>();
+ myTouched.addAll(removedIconStates);
+ myTouched.addAll(newIconStates.keySet());
+ myTouched.addAll(modifiedIconStates.keySet());
+
+ HashSet otherTouched = new HashSet<>();
+ otherTouched.addAll(other.removedIconStates);
+ otherTouched.addAll(other.newIconStates.keySet());
+ otherTouched.addAll(other.modifiedIconStates.keySet());
+
+ HashSet bothTouched = (HashSet)myTouched.clone();
+ bothTouched.retainAll(otherTouched); // this set now contains the list of icon_states that *both* diffs modified, which we'll put in conflictDMI for manual merge (unless they were deletions
+
+ if(Main.VERBOSITY > 0) {
+ System.out.println("a: " + Arrays.toString(myTouched.toArray()));
+ System.out.println("b: " + Arrays.toString(otherTouched.toArray()));
+ System.out.println("both: " + Arrays.toString(bothTouched.toArray()));
+ }
+
+ HashSet whatHappened = new HashSet<>();
+
+ for(String s: bothTouched) {
+ String here, there;
+ if(removedIconStates.contains(s)) {
+ here = "removed";
+ } else if(newIconStates.containsKey(s)) {
+ here = "added";
+ } else if(modifiedIconStates.containsKey(s)) {
+ here = "modified";
+ } else {
+ System.out.println("Unknown error; state="+s);
+ here = "???";
+ }
+
+ if(other.removedIconStates.contains(s)) {
+ there = "removed";
+ } else if(other.newIconStates.containsKey(s)) {
+ there = "added";
+ } else if(other.modifiedIconStates.containsKey(s)) {
+ there = "modified";
+ } else {
+ System.out.println("Unknown error; state="+s);
+ there = "???";
+ }
+
+ whatHappened.add(s + ": " + here + "|" + there);
+ }
+
+ // Removals
+ for(String s: removedIconStates) {
+ if(!bothTouched.contains(s)) {
+ merged.removedIconStates.add(s);
+ }
+ }
+ for(String s: other.removedIconStates) {
+ if(!bothTouched.contains(s)) {
+ merged.removedIconStates.add(s);
+ }
+ }
+
+ // Modifications
+ for(String s: modifiedIconStates.keySet()) {
+ if(!bothTouched.contains(s)) {
+ merged.modifiedIconStates.put(s, modifiedIconStates.get(s));
+ } else {
+ conflictDMI.addIconState(aName + "|" + s, modifiedIconStates.get(s).newState);
+ }
+ }
+ for(String s: other.modifiedIconStates.keySet()) {
+ if(!bothTouched.contains(s)) {
+ merged.modifiedIconStates.put(s, other.modifiedIconStates.get(s));
+ } else {
+ conflictDMI.addIconState(bName + "|" + s, other.modifiedIconStates.get(s).newState);
+ }
+ }
+
+ // Additions
+ for(String s: newIconStates.keySet()) {
+ if(!bothTouched.contains(s)) {
+ merged.newIconStates.put(s, newIconStates.get(s));
+ } else {
+ conflictDMI.addIconState(aName + s, newIconStates.get(s));
+ }
+ }
+ for(String s: other.newIconStates.keySet()) {
+ if(!bothTouched.contains(s)) {
+ merged.newIconStates.put(s, other.newIconStates.get(s));
+ } else {
+ conflictDMI.addIconState(bName + s, other.newIconStates.get(s));
+ }
+ }
+
+ return whatHappened;
+ }
+
+ @Override public String toString() {
+ String s = "";
+ String t = "\t";
+ String q = "\"";
+ String n = "\n";
+ if(!removedIconStates.isEmpty()) {
+ s += "Removed:\n";
+ for(String state: removedIconStates)
+ s += t + q + state + q + n;
+ }
+ if(!modifiedIconStates.isEmpty()) {
+ s += "Modified:\n";
+ for(String state: modifiedIconStates.keySet())
+ s += t + q + state + q + " [" + modifiedIconStates.get(state).toString() + "]\n";
+ }
+ if(!newIconStates.isEmpty()) {
+ s += "Added:\n";
+ for(String state: newIconStates.keySet())
+ s += t + q + state + q + " " + newIconStates.get(state).infoStr() + n;
+ }
+ if("".equals(s))
+ return "No changes";
+ return s;
+ }
+}
\ No newline at end of file
diff --git a/tools/dmitool/src/main/java/dmitool/DMIException.java b/tools/dmitool/src/main/java/dmitool/DMIException.java
new file mode 100644
index 00000000000..e9293162894
--- /dev/null
+++ b/tools/dmitool/src/main/java/dmitool/DMIException.java
@@ -0,0 +1,24 @@
+package dmitool;
+
+public class DMIException extends Exception {
+ String[] desc = null;
+ int line = 0;
+ public DMIException(String[] descriptor, int line, String what) {
+ super(what);
+ desc = descriptor;
+ this.line = line;
+ }
+ public DMIException(String what) {
+ super(what);
+ }
+ public DMIException(String what, Exception cause) {
+ super(what, cause);
+ }
+
+ @Override public String getMessage() {
+ if(desc != null)
+ return "\"" + desc[line] + "\" - " + super.getMessage();
+
+ return super.getMessage();
+ }
+}
\ No newline at end of file
diff --git a/tools/dmitool/src/main/java/dmitool/IconState.java b/tools/dmitool/src/main/java/dmitool/IconState.java
new file mode 100644
index 00000000000..2a2202c71cd
--- /dev/null
+++ b/tools/dmitool/src/main/java/dmitool/IconState.java
@@ -0,0 +1,280 @@
+package dmitool;
+
+import java.util.Arrays;
+import ar.com.hjg.pngj.ImageInfo;
+import ar.com.hjg.pngj.ImageLineInt;
+import ar.com.hjg.pngj.PngWriter;
+import ar.com.hjg.pngj.PngReader;
+import ar.com.hjg.pngj.PngjInputException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+public class IconState {
+ String name;
+ int dirs;
+ int frames;
+ float[] delays;
+ Image[] images; // dirs come first
+ boolean rewind;
+ int loop;
+ String hotspot;
+ boolean movement;
+
+ public String getInfoLine() {
+ String extraInfo = "";
+ if(rewind) extraInfo += " rewind";
+ if(frames != 1) {
+ extraInfo += " loop(" + (loop==-1 ? "infinite" : loop) + ")";
+ }
+ if(hotspot != null) extraInfo += " hotspot('" + hotspot + "')";
+ if(movement) extraInfo += " movement";
+ if(extraInfo.equals("")) {
+ return String.format("state \"%s\", %d dir(s), %d frame(s)", name, dirs, frames);
+ } else {
+ return String.format("state \"%s\", %d dir(s), %d frame(s),%s", name, dirs, frames, extraInfo);
+ }
+ }
+
+ @Override public IconState clone() {
+ IconState is = new IconState(name, dirs, frames, images.clone(), delays==null ? null : delays.clone(), rewind, loop, hotspot, movement);
+ is.delays = delays != null ? delays.clone() : null;
+ is.rewind = rewind;
+
+ return is;
+ }
+
+ public IconState(String name, int dirs, int frames, Image[] images, float[] delays, boolean rewind, int loop, String hotspot, boolean movement) {
+ if(delays != null) {
+ if(Main.STRICT && delays.length != frames) {
+ throw new IllegalArgumentException("Delays and frames must be the same length!");
+ }
+ }
+ this.name = name;
+ this.dirs = dirs;
+ this.frames = frames;
+ this.images = images;
+ this.rewind = rewind;
+ this.loop = loop;
+ this.hotspot = hotspot;
+ this.delays = delays;
+ this.movement = movement;
+ }
+ void setDelays(float[] delays) {
+ this.delays = delays;
+ }
+ void setRewind(boolean b) {
+ rewind = b;
+ }
+ @Override public boolean equals(Object obj) {
+ if(obj == this) return true;
+ if(!(obj instanceof IconState)) return false;
+
+ IconState is = (IconState)obj;
+
+ if(!is.name.equals(name)) return false;
+ if(is.dirs != dirs) return false;
+ if(is.frames != frames) return false;
+ if(!Arrays.equals(images, is.images)) return false;
+ if(is.rewind != rewind) return false;
+ if(is.loop != loop) return false;
+ if(!Arrays.equals(delays, is.delays)) return false;
+ if(!(is.hotspot == null ? hotspot == null : is.hotspot.equals(hotspot))) return false;
+ if(is.movement != movement) return false;
+
+ return true;
+ }
+ public String infoStr() {
+ return "[" + frames + " frame(s), " + dirs + " dir(s)]";
+ }
+ public String getDescriptorFragment() {
+ String s = "";
+ String q = "\"";
+ String n = "\n";
+ s += "state = " + q + name + q + n;
+ s += "\tdirs = " + dirs + n;
+ s += "\tframes = " + frames + n;
+ if(delays != null) {
+ s += "\tdelay = " + delayArrayToString(delays) + n;
+ }
+ if(rewind) {
+ s += "\trewind = 1\n";
+ }
+ if(loop != -1) {
+ s += "\tloop = " + loop + n;
+ }
+ if(hotspot != null) {
+ s += "\thotspot = " + hotspot + n;
+ }
+ if(movement) {
+ s += "\tmovement = 1\n";
+ }
+ return s;
+ }
+
+ private static String delayArrayToString(float[] d) {
+ String s = "";
+ for(float f: d) {
+ s += ","+f;
+ }
+ return s.substring(1);
+ }
+
+ /**
+ * Dump the state to the given OutputStream in PNG format. Frames will be dumped along the X axis of the image, and directions will be dumped along the Y.
+ */
+ public void dumpToPNG(OutputStream outS, int minDir, int maxDir, int minFrame, int maxFrame) {
+ int totalDirs = maxDir - minDir + 1;
+ int totalFrames = maxFrame - minFrame + 1;
+
+ int w = images[minDir + minFrame * this.dirs].w;
+ int h = images[minDir + minFrame * this.dirs].h;
+
+ if(Main.VERBOSITY > 0) System.out.println("Writing " + totalDirs + " dir(s), " + totalFrames + " frame(s), " + totalDirs*totalFrames + " image(s) total.");
+ ImageInfo ii = new ImageInfo(totalFrames * w, totalDirs * h, 8, true);
+ PngWriter out = new PngWriter(outS, ii);
+ out.setCompLevel(9);
+
+ Image[][] img = new Image[totalFrames][totalDirs];
+ {
+ for(int i=0; i= frames)
+ throw new IllegalArgumentException("Provided frame is out of range: " + frame);
+ if(dir < 0 || dir >= dirs)
+ throw new IllegalArgumentException("Provided dir is out of range: " + dir);
+
+ images[getIndex(dir, frame)] = splice;
+ }
+}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tools/dmitool/src/main/java/dmitool/IconStateDiff.java b/tools/dmitool/src/main/java/dmitool/IconStateDiff.java
new file mode 100644
index 00000000000..a5d30db7fe0
--- /dev/null
+++ b/tools/dmitool/src/main/java/dmitool/IconStateDiff.java
@@ -0,0 +1,126 @@
+package dmitool;
+
+import java.util.HashMap;
+import java.util.HashSet;
+
+public class IconStateDiff {
+ static class ISAddress {
+ int dir;
+ int frame;
+
+ public ISAddress(int dir, int frame) {
+ this.dir = dir;
+ this.frame = frame;
+ }
+
+ public String infoStr(int maxDir, int maxFrame) {
+ if(maxDir == 1 && maxFrame == 1) {
+ return "";
+ } else if(maxDir == 1) {
+ return "{" + frame + "}";
+ } else if(maxFrame == 1) {
+ return "{" + Main.dirs[dir] + "}";
+ } else {
+ return "{" + Main.dirs[dir] + " " + frame + "}";
+ }
+ }
+ }
+ int oldFrameCount = 0;
+ int oldDirectionCount = 0;
+ boolean oldRewind = false;
+ int oldLoop = -1;
+ String oldHotspot = null;
+
+ int newFrameCount = 0;
+ int newDirectionCount = 0;
+ boolean newRewind = false;
+ int newLoop = -1;
+ String newHotspot = null;
+
+ IconState newState;
+ HashMap modifiedFrames = new HashMap<>();
+ HashMap newFrames = new HashMap<>();
+ HashSet removedFrames = new HashSet<>();
+
+ public IconStateDiff(IconState base, IconState mod) {
+ int maxDir = Math.max(base.dirs, mod.dirs);
+ int maxFrame = Math.max(base.frames, mod.frames);
+
+ oldFrameCount = base.frames;
+ oldDirectionCount = base.dirs;
+ oldRewind = base.rewind;
+ oldLoop = base.loop;
+ oldHotspot = base.hotspot;
+
+ newFrameCount = mod.frames;
+ newDirectionCount = mod.dirs;
+ newRewind = mod.rewind;
+ newLoop = mod.loop;
+ newHotspot = mod.hotspot;
+
+ newState = mod;
+
+ Image baseI, modI;
+ for(int d=0; d d && base.frames > f) {
+ baseI = base.images[f * base.dirs + d];
+ } else baseI = null;
+ if(mod.dirs > d && mod.frames > f) {
+ modI = mod.images[f * mod.dirs + d];
+ } else modI = null;
+
+ if(baseI == null && modI == null) continue;
+
+ if(baseI == null) newFrames.put(new ISAddress(d, f), modI);
+ else if(modI == null) removedFrames.add(new ISAddress(d, f));
+ else if(!baseI.equals(modI)) {
+ modifiedFrames.put(new ISAddress(d, f), modI);
+ }
+ }
+ }
+ }
+
+ @Override public String toString() {
+ String s = "";
+ String tmp;
+
+ if(newDirectionCount != oldDirectionCount)
+ s += " | dirs " + oldDirectionCount + "->" + newDirectionCount;
+
+ if(newFrameCount != oldFrameCount)
+ s += " | frames " + oldFrameCount + "->" + newFrameCount;
+
+ if(newRewind != oldRewind) {
+ s += " | rewind " + oldRewind + "->" + newRewind;
+ }
+
+ if(newLoop != oldLoop) {
+ s += " | loop " + oldLoop + "->" + newLoop;
+ }
+
+ if(newHotspot == null ? oldHotspot != null : !newHotspot.equals(oldHotspot)) {
+ s += " | hotspot " + oldHotspot + "->" + newHotspot;
+ }
+
+ if(!modifiedFrames.isEmpty()) {
+ int total_frames = Math.min(oldFrameCount, newFrameCount) * Math.min(oldDirectionCount, newDirectionCount);
+ tmp = "";
+ for(ISAddress isa: modifiedFrames.keySet()) {
+ String str = isa.infoStr(oldDirectionCount, oldFrameCount);
+ if(!"".equals(str)) {
+ tmp += ", " + str;
+ }
+ }
+ if(!"".equals(tmp)) {
+ s += " | modified " + modifiedFrames.size() + " of " + total_frames + ": " + tmp.substring(1);
+ } else {
+ s += " | modified " + modifiedFrames.size() + " of " + total_frames;
+ }
+ }
+
+ if("".equals(s))
+ return "No change";
+ return s.substring(3);
+ }
+}
\ No newline at end of file
diff --git a/tools/dmitool/src/main/java/dmitool/Image.java b/tools/dmitool/src/main/java/dmitool/Image.java
new file mode 100644
index 00000000000..2ea7ff41920
--- /dev/null
+++ b/tools/dmitool/src/main/java/dmitool/Image.java
@@ -0,0 +1,32 @@
+package dmitool;
+
+import java.io.IOException;
+import java.io.OutputStream;
+
+public abstract class Image {
+ int w, h;
+
+ abstract RGBA getPixel(int x, int y);
+
+ public Image(int w, int h) {
+ this.w = w;
+ this.h = h;
+ }
+
+ @Override public boolean equals(Object obj) {
+ if(obj == this) return true;
+ if(!(obj instanceof Image)) return false;
+
+ Image im = (Image) obj;
+
+ if(w != im.w || h != im.h) return false;
+
+ for(int i=0; i argq = new ArrayDeque<>();
+ for(String s: args) {
+ argq.addLast(s);
+ }
+ if(argq.size() == 0) {
+ System.out.println("No command found; use 'help' for help");
+ return;
+ }
+ String switches = argq.peekFirst();
+ if(switches.startsWith("-")) {
+ for(char c: switches.substring(1).toCharArray()) {
+ switch(c) {
+ case 'v': VERBOSITY++; break;
+ case 'q': VERBOSITY--; break;
+ case 'S': STRICT = true; break;
+ }
+ }
+ argq.pollFirst();
+ }
+ String op = argq.pollFirst();
+
+ switch(op) {
+ case "diff": {
+ if(argq.size() < 2) {
+ System.out.println("Insufficient arguments for command!");
+ System.out.println(helpStr);
+ return;
+ }
+ String a = argq.pollFirst();
+ String b = argq.pollFirst();
+
+ if(VERBOSITY >= 0) System.out.println("Loading " + a);
+ DMI dmi = doDMILoad(a);
+ if(VERBOSITY >= 0) dmi.printInfo();
+
+ if(VERBOSITY >= 0) System.out.println("Loading " + b);
+ DMI dmi2 = doDMILoad(b);
+ if(VERBOSITY >= 0) dmi2.printInfo();
+
+ DMIDiff dmid = new DMIDiff(dmi, dmi2);
+ System.out.println(dmid);
+ break;
+ }
+ case "sort": {
+ if(argq.size() < 1) {
+ System.out.println("Insufficient arguments for command!");
+ System.out.println(helpStr);
+ return;
+ }
+ String f = argq.pollFirst();
+
+ if(VERBOSITY >= 0) System.out.println("Loading " + f);
+ DMI dmi = doDMILoad(f);
+ if(VERBOSITY >= 0) dmi.printInfo();
+
+ if(VERBOSITY >= 0) System.out.println("Saving " + f);
+ dmi.writeDMI(new FileOutputStream(f), true);
+ break;
+ }
+ case "merge": {
+ if(argq.size() < 4) {
+ System.out.println("Insufficient arguments for command!");
+ System.out.println(helpStr);
+ return;
+ }
+ String baseF = argq.pollFirst(),
+ aF = argq.pollFirst(),
+ bF = argq.pollFirst(),
+ mergedF = argq.pollFirst();
+ if(VERBOSITY >= 0) System.out.println("Loading " + baseF);
+ DMI base = doDMILoad(baseF);
+ if(VERBOSITY >= 0) base.printInfo();
+
+ if(VERBOSITY >= 0) System.out.println("Loading " + aF);
+ DMI aDMI = doDMILoad(aF);
+ if(VERBOSITY >= 0) aDMI.printInfo();
+
+ if(VERBOSITY >= 0) System.out.println("Loading " + bF);
+ DMI bDMI = doDMILoad(bF);
+ if(VERBOSITY >= 0) bDMI.printInfo();
+
+ DMIDiff aDiff = new DMIDiff(base, aDMI);
+ DMIDiff bDiff = new DMIDiff(base, bDMI);
+ DMIDiff mergedDiff = new DMIDiff();
+ DMI conflictDMI = new DMI(32, 32);
+
+ Set cf = aDiff.mergeDiff(bDiff, conflictDMI, mergedDiff, aF, bF);
+
+ mergedDiff.applyToDMI(base);
+
+ base.writeDMI(new FileOutputStream(mergedF));
+
+ if(!cf.isEmpty()) {
+ if(VERBOSITY >= 0) for(String s: cf) {
+ System.out.println(s);
+ }
+ conflictDMI.writeDMI(new FileOutputStream(mergedF + ".conflict.dmi"), true);
+ System.out.println("Add/modify conflicts placed in '" + mergedF + ".conflict.dmi'");
+ System.exit(1); // Git expects non-zero on merge conflict
+ } else {
+ System.out.println("No conflicts");
+ System.exit(0);
+ }
+ break;
+ }
+ case "extract": {
+ if(argq.size() < 3) {
+ System.out.println("Insufficient arguments for command!");
+ System.out.println(helpStr);
+ return;
+ }
+ String file = argq.pollFirst(),
+ state = argq.pollFirst(),
+ outFile = argq.pollFirst();
+
+ DMI dmi = doDMILoad(file);
+ if(VERBOSITY >= 0) dmi.printInfo();
+
+ IconState is = dmi.getIconState(state);
+ if(is == null) {
+ System.out.println("icon_state '"+state+"' does not exist!");
+ return;
+ }
+ // minDir, Maxdir, minFrame, Maxframe
+ int mDir=0, Mdir=is.dirs-1;
+ int mFrame=0, Mframe=is.frames-1;
+
+ while(argq.size() > 1) {
+ String arg = argq.pollFirst();
+
+ switch(arg) {
+ case "d":
+ case "dir":
+ case "dirs":
+ case "direction":
+ case "directions":
+ String dString = argq.pollFirst();
+ if(dString.contains("-")) {
+ String[] splitD = dString.split("-");
+ if(splitD.length == 2) {
+ mDir = parseDir(splitD[0], is);
+ Mdir = parseDir(splitD[1], is);
+ } else {
+ System.out.println("Illegal dir string: '" + dString + "'!");
+ return;
+ }
+ } else {
+ mDir = parseDir(dString, is);
+ Mdir = mDir;
+ }
+ // Invalid value check, warnings are printed in parseDir()
+ if(mDir == -1 || Mdir == -1) return;
+ if(Mdir < mDir) {
+ System.out.println("Maximum dir greater than minimum dir!");
+ System.out.println("Textual direction order is S, N, E, W, SE, SW, NE, NW increasing 0 (S) to 7 (NW)");
+ return;
+ }
+ break;
+ case "f":
+ case "frame":
+ case "frames":
+ String fString = argq.pollFirst();
+ if(fString.contains("-")) {
+ String[] splitF = fString.split("-");
+ if(splitF.length == 2) {
+ mFrame = parseFrame(splitF[0], is);
+ Mframe = parseFrame(splitF[1], is);
+ } else {
+ System.out.println("Illegal frame string: '" + fString + "'!");
+ return;
+ }
+ } else {
+ mFrame = parseFrame(fString, is);
+ Mframe = mFrame;
+ }
+ // Invalid value check, warnings are printed in parseFrame()
+ if(mFrame == -1 || Mframe == -1) return;
+ if(Mframe < mFrame) {
+ System.out.println("Maximum frame greater than minimum frame!");
+ return;
+ }
+ break;
+ default:
+ System.out.println("Unknown argument '" + arg + "' detected, ignoring.");
+ }
+ }
+ if(!argq.isEmpty()) {
+ System.out.println("Extra argument '" + argq.pollFirst() + "' detected, ignoring.");
+ }
+ is.dumpToPNG(new FileOutputStream(outFile), mDir, Mdir, mFrame, Mframe);
+ break;
+ }
+ case "import": {
+ if(argq.size() < 3) {
+ System.out.println("Insufficient arguments for command!");
+ System.out.println(helpStr);
+ return;
+ }
+ String dmiFile = argq.pollFirst(),
+ stateName = argq.pollFirst(),
+ pngFile = argq.pollFirst();
+
+ boolean noDup = false;
+ boolean rewind = false;
+ int loop = 0;
+ boolean movement = false;
+ String hotspot = null;
+ float[] delays = null;
+ String replaceDir = null;
+ String replaceFrame = null;
+ while(!argq.isEmpty()) {
+ String s = argq.pollFirst();
+ switch(s.toLowerCase()) {
+ case "nodup":
+ case "nd":
+ case "n":
+ noDup = true;
+ break;
+ case "rewind":
+ case "rw":
+ case "r":
+ rewind = true;
+ break;
+ case "loop":
+ case "lp":
+ case "l":
+ loop = -1;
+ break;
+ case "loopn":
+ case "lpn":
+ case "ln":
+ if(!argq.isEmpty()) {
+ String loopTimes = argq.pollFirst();
+ try {
+ loop = Integer.parseInt(loopTimes);
+ } catch(NumberFormatException nfe) {
+ System.out.println("Illegal number '" + loopTimes + "' as argument to '" + s + "'!");
+ return;
+ }
+ } else {
+ System.out.println("Argument '" + s + "' requires a numeric argument following it!");
+ return;
+ }
+ break;
+ case "movement":
+ case "move":
+ case "mov":
+ case "m":
+ movement = true;
+ break;
+ case "delays":
+ case "delay":
+ case "del":
+ case "d":
+ if(!argq.isEmpty()) {
+ String delaysString = argq.pollFirst();
+ String[] delaysSplit = delaysString.split(",");
+ delays = new float[delaysSplit.length];
+ for(int i=0; i= 0) System.out.println("Loading " + dmiFile);
+ DMI toImportTo = doDMILoad(dmiFile);
+ if(VERBOSITY >= 0) toImportTo.printInfo();
+ IconState is = IconState.importFromPNG(toImportTo, new FileInputStream(pngFile), stateName, delays, rewind, loop, hotspot, movement);
+
+ //image insertion
+ if(replaceDir != null || replaceFrame != null) {
+
+ IconState targetIs = toImportTo.getIconState(stateName);
+ if(targetIs == null) {
+ System.out.println("'direction' or 'frame' specified and no icon state '" + stateName + "' found, aborting!");
+ return;
+ }
+ if(is.images.length == 0) {
+ System.out.println("'direction' or 'frame' specified and imported is empty, aborting!");
+ return;
+ }
+
+ if(!noDup) targetIs = targetIs.clone();
+
+ int dirToReplace, frameToReplace;
+ if(replaceDir != null && replaceFrame != null) {
+ frameToReplace = parseFrame(replaceFrame, targetIs);
+ dirToReplace = parseDir(replaceDir, targetIs);
+ targetIs.insertImage(dirToReplace, frameToReplace, is.images[0]);
+ }
+ else if(replaceDir != null) {
+ dirToReplace = parseDir(replaceDir, targetIs);
+ targetIs.insertDir(dirToReplace, is.images);
+ }
+ else if(replaceFrame != null) {
+ frameToReplace = parseFrame(replaceFrame, targetIs);
+ targetIs.insertFrame(frameToReplace, is.images);
+ }
+
+ if(!noDup) toImportTo.addIconState(null, targetIs);
+ }
+ else {
+ if(noDup) {
+ if(!toImportTo.setIconState(is)) {
+ toImportTo.addIconState(null, is);
+ }
+ } else {
+ toImportTo.addIconState(null, is);
+ }
+ }
+
+ if(VERBOSITY >= 0) toImportTo.printInfo();
+
+ if(VERBOSITY >= 0) System.out.println("Saving " + dmiFile);
+ toImportTo.writeDMI(new FileOutputStream(dmiFile));
+ break;
+ }
+ case "verify": {
+ if(argq.size() < 1) {
+ System.out.println("Insufficient arguments for command!");
+ System.out.println(helpStr);
+ return;
+ }
+ String vF = argq.pollFirst();
+ if(VERBOSITY >= 0) System.out.println("Loading " + vF);
+ DMI v = doDMILoad(vF);
+ if(VERBOSITY >= 0) v.printInfo();
+ break;
+ }
+ case "info": {
+ if(argq.size() < 1) {
+ System.out.println("Insufficient arguments for command!");
+ System.out.println(helpStr);
+ return;
+ }
+ String infoFile = argq.pollFirst();
+ if(VERBOSITY >= 0) System.out.println("Loading " + infoFile);
+ DMI info = doDMILoad(infoFile);
+ info.printInfo();
+ info.printStateList();
+ break;
+ }
+ case "version":
+ System.out.println(VERSION);
+ return;
+ default:
+ System.out.println("Command '" + op + "' not found!");
+ case "help":
+ System.out.println(helpStr);
+ break;
+ }
+ }
+
+ static int parseDir(String s, IconState is) {
+ try {
+ int i = Integer.parseInt(s);
+ if(0 <= i && i < is.dirs) {
+ return i;
+ } else {
+ System.out.println("Direction not in valid range [0, "+(is.dirs-1)+"]!");
+ return -1;
+ }
+ } catch(NumberFormatException nfe) {
+ for(int q=0; q