code:
/* Runtime Condenser by Nodrak
 * This will sum up identical runtimes into one, giving a total of how many times it occured. The first occurance
 * of the runtime will log the proc, source, usr and src, the rest will just add to the total. Infinite loops will
 * also be caught and displayed (if any) above the list of runtimes.
 *
 * How to use:
 * 1) Copy and paste your list of runtimes from Dream Daemon into input.exe
 * 2) Run RuntimeCondenser.exe
 * 3) Open output.txt for a condensed report of the runtimes
 */

#include <iostream>
#include <fstream>
#include <string>
#include <sstream>

using namespace std;

//Make all of these global. It's bad yes, but it's a small program so it really doesn't affect anything.
	//Because hardcoded numbers are bad :(
	const unsigned short maxStorage = 99; //100 - 1

	//What we use to read input
	string currentLine = "Blank";
	string nextLine = "Blank";

	//Stores lines we want to keep to print out
	string storedRuntime[maxStorage+1];
	string storedProc[maxStorage+1];
	string storedSource[maxStorage+1];
	string storedUsr[maxStorage+1];
	string storedSrc[maxStorage+1];

	string storedHardDel[maxStorage+1];

	//Stat tracking stuff for output
	unsigned int totalRuntimes = 0;
	unsigned int totalUniqueRuntimes = 0;
	unsigned int totalInfiniteLoops = 0;
	unsigned int totalUniqueInfiniteLoops = 0;
	unsigned int totalHardDels = 0;
	unsigned int totalUniqueHardDels = 0;

	//Misc
	unsigned int numRuntime[maxStorage+1]; //Number of times a specific runtime has occured
	unsigned int numHardDel[maxStorage+1]; //Number of times a specific hard del has occured
	bool checkNextLines = false; //Used in case byond has condensed a large number of similar runtimes
	int storedIterator = 0; //Used to remember where we stored the runtime

bool readFromFile()
{
	//Open file to read
	ifstream inputFile("input.txt");

	if(inputFile.is_open())
	{
		while(!inputFile.eof()) //Until end of file
		{
			//If we've run out of storage
			if(storedRuntime[maxStorage] != "Blank") break;

			//Update our lines
			currentLine	= nextLine;
			getline(inputFile, nextLine);

			//After finding a new runtime, check to see if there are extra values to store
			if(checkNextLines)
			{
				//Skip ahead
				currentLine = nextLine;
				getline(inputFile, nextLine);

				//If we find this, we have new stuff to store
				if(nextLine.find("usr:") != std::string::npos)
				{
					//Store more info
					storedSource[storedIterator] = currentLine;
					storedUsr[storedIterator] = nextLine;

					//Skip ahead again
					currentLine = nextLine;
					getline(inputFile, nextLine);

					//Store the last of the info
					storedSrc[storedIterator] = nextLine;
				}
				checkNextLines = false;
			}

			//Found an infinite loop!
			if(currentLine.find("Infinite loop suspected") != std::string::npos || currentLine.find("Maximum recursion level reached") != std::string::npos)
			{
				totalInfiniteLoops++;

				for(int i=0; i <= maxStorage; i++)
				{
					//We've already encountered this
					if(currentLine == storedRuntime[i])
					{
						numRuntime[i]++;
						break;
					}

					//We've never encountered this
					if(storedRuntime[i] == "Blank")
					{
						storedRuntime[i] = currentLine;
						currentLine = nextLine;
						getline(inputFile, nextLine); //Skip the "if this is not an infinite loop" line
						storedProc[i] = nextLine;
						numRuntime[i] = 1;
						checkNextLines = true;
						storedIterator = i;
						totalUniqueInfiniteLoops++;
						break;
					}
				}
			}
			//Found a runtime!
			else if(currentLine.find("runtime error:") != std::string::npos)
			{
				totalRuntimes++;
				for(int i=0; i <= maxStorage; i++)
				{
					//We've already encountered this
					if(currentLine == storedRuntime[i])
					{
						numRuntime[i]++;
						break;
					}

					//We've never encountered this
					if(storedRuntime[i] == "Blank")
					{
						storedRuntime[i] = currentLine;
						storedProc[i] = nextLine;
						numRuntime[i] = 1;
						checkNextLines = true;
						storedIterator = i;
						totalUniqueRuntimes++;
						break;
					}
				}
			}

			//Found a hard del!
			else if(currentLine.find("Path :") != std::string::npos)
			{
				//this is pretty ugly but the alternative was implementing regex which I haven't the slightest idea how to do
				//it takes advantage of the formatting of the line to extract the amount of failures
				std::string tmp;
				char c;
				int failures;
				std::stringstream ss(nextLine);
				ss >> tmp >> c >> failures;

				totalHardDels += failures;

				for(int i=0; i <= maxStorage; i++)
				{

					//We've already encountered this
					if(currentLine == storedHardDel[i])
					{
						numHardDel[i] += failures;
						break;
					}

					//We've never encountered this
					if(storedHardDel[i] == "Blank")
					{
						storedHardDel[i] = currentLine;
						numHardDel[i] = failures;
						checkNextLines = true;
						totalUniqueHardDels++;
						break;
					}
				}
			}
		}
	}
	else
	{
		return false;
	}
	return true;
}

bool writeToFile()
{
	//Open and clear the file
	ofstream outputFile("Output.txt", ios::trunc);

	if(outputFile.is_open())
	{
		outputFile << "Note: The proc name, source file, src and usr are all from the FIRST of the identical runtimes. Everything else is cropped.\n\n";
		if(totalUniqueInfiniteLoops > 0)
		{
			outputFile << "Total unique infinite loops: " << totalUniqueInfiniteLoops << endl;
		}
		if(totalInfiniteLoops > 0)
		{
			outputFile << "Total infinite loops: " << totalInfiniteLoops << endl << endl;
		}
		outputFile << "Total unique runtimes: " << totalUniqueRuntimes << endl;
		outputFile << "Total runtimes: " << totalRuntimes << endl << endl;
		if(totalUniqueHardDels > 0)
		{
			outputFile << "Total unique hard deletions: " << totalUniqueHardDels << endl;
		}
		if(totalHardDels > 0)
		{
			outputFile << "Total hard deletions: " << totalHardDels << endl << endl;
		}

		//Display a warning if we've hit the maximum space we've allocated for storage
		if(totalUniqueRuntimes + totalUniqueInfiniteLoops >= maxStorage)
		{
			outputFile << "Warning: The maximum number of unique runtimes has been hit. If there were more, they have been cropped out.\n\n";
		}
		if(totalUniqueHardDels >= maxStorage)
		{
			outputFile << "Warning: The maximum number of unique hard deletions has been hit. If there were more, they have been croped out.\n\n";
		}

		//If we have infinite loops, display them first.
		if(totalInfiniteLoops > 0)
		{
			outputFile << "** Infinite loops **";
			for(int i=0; i <= maxStorage; i++)
			{
				if(storedRuntime[i].find("Infinite loop suspected") != std::string::npos || storedRuntime[i].find("Maximum recursion level reached") != std::string::npos)
				{
					if(numRuntime[i] != 0) outputFile << endl << endl << "The following infinite loop has occured " << numRuntime[i] << " time(s).\n";
					if(storedRuntime[i] != "Blank") outputFile << storedRuntime[i] << endl;
					if(storedProc[i] != "Blank") outputFile << storedProc[i] << endl;
					if(storedSource[i] != "Blank") outputFile << storedSource[i] << endl;
					if(storedUsr[i] != "Blank") outputFile << storedUsr[i] << endl;
					if(storedSrc[i] != "Blank") outputFile << storedSrc[i] << endl;
				}
			}
			outputFile << endl << endl; //For spacing
		}

		//Do runtimes next
		outputFile << "** Runtimes **";
		for(int i=0; i <= maxStorage; i++)
		{
			if(storedRuntime[i].find("Infinite loop suspected") != std::string::npos || storedRuntime[i].find("Maximum recursion level reached") != std::string::npos) continue;

			if(numRuntime[i] != 0) outputFile << endl << endl << "The following runtime has occured " << numRuntime[i] << " time(s).\n";
			if(storedRuntime[i] != "Blank") outputFile << storedRuntime[i] << endl;
			if(storedProc[i] != "Blank") outputFile << storedProc[i] << endl;
			if(storedSource[i] != "Blank") outputFile << storedSource[i] << endl;
			if(storedUsr[i] != "Blank") outputFile << storedUsr[i] << endl;
			if(storedSrc[i] != "Blank") outputFile << storedSrc[i] << endl;
		}

		//and finally, hard deletes
		if(totalHardDels > 0)
		{
			outputFile << endl << "** Hard deletions **";
			for(int i=0; i <= maxStorage; i++)
			{
				if(numHardDel[i] != 0 && storedHardDel[i] != "Blank") outputFile << endl << storedHardDel[i] << " - " << numHardDel[i] << " time(s).\n";
			}
		}
		outputFile.close();
	}
	else
	{
		return false;
	}
	return true;
}

void sortRuntimes()
{
	string tempRuntime[maxStorage+1];
	string tempProc[maxStorage+1];
	string tempSource[maxStorage+1];
	string tempUsr[maxStorage+1];
	string tempSrc[maxStorage+1];
	unsigned int tempNumRuntime[maxStorage+1];
	unsigned int highestCount = 0; //Used for descending order
//	int keepLooping = 0;

	//Move all of our data into temporary arrays. Also clear the stored data (not necessary but.. just incase)
	for(int i=0; i <= maxStorage; i++)
	{
		//Get the largest occurance of a single runtime
		if(highestCount < numRuntime[i])
		{
			highestCount = numRuntime[i];
		}

		tempRuntime[i] = storedRuntime[i];	storedRuntime[i] = "Blank";
		tempProc[i] = storedProc[i];		storedProc[i] = "Blank";
		tempSource[i] = storedSource[i];	storedSource[i] = "Blank";
		tempUsr[i] = storedUsr[i];			storedUsr[i] = "Blank";
		tempSrc[i] = storedSrc[i];			storedSrc[i] = "Blank";
		tempNumRuntime[i] = numRuntime[i];	numRuntime[i] = 0;
	}

	while(highestCount > 0)
	{
		for(int i=0; i <= maxStorage; i++) //For every runtime
		{
			if(tempNumRuntime[i] == highestCount) //If the number of occurances of that runtime is equal to our current highest
			{
				for(int j=0; j <= maxStorage; j++) //Find the next available slot and store the info
				{
					if(storedRuntime[j] == "Blank") //Found an empty spot
					{
						storedRuntime[j] = tempRuntime[i];
						storedProc[j] = tempProc[i];
						storedSource[j] = tempSource[i];
						storedUsr[j] = tempUsr[i];
						storedSrc[j] = tempSrc[i];
						numRuntime[j] = tempNumRuntime[i];
						break;
					}
				}
			}
		}
		highestCount--; //Lower our 'highest' by one and continue
	}
}

void sortHardDels() //copypasting and I don't care~
{
	string tempHardDel[maxStorage + 1];
	unsigned int tempNumHardDel[maxStorage + 1];
	unsigned int highestCount = 0;

	for (int i = 0; i <= maxStorage; i++)
	{
		//Get the largest occurance of a single hard deletion
		if (highestCount < numHardDel[i])
		{
			highestCount = numHardDel[i];
		}

		tempHardDel[i] = storedHardDel[i];	storedHardDel[i] = "Blank";
		tempNumHardDel[i] = numHardDel[i];	numHardDel[i] = 0;
	}

	while (highestCount > 0)
	{
		for (int i = 0; i <= maxStorage; i++) //For every hard deletion
		{
			if (tempNumHardDel[i] == highestCount) //If the number of occurances of that hard deletion is equal to our current highest
			{
				for (int j = 0; j <= maxStorage; j++) //Find the next available slot and store the info
				{
					if (storedHardDel[j] == "Blank") //Found an empty spot
					{
						storedHardDel[j] = tempHardDel[i];
						numHardDel[j] = tempNumHardDel[i];
						break;
					}
				}
			}
		}
		highestCount--; //Lower our 'highest' by one and continue
	}
}

int main() {
	char exit; //Used to stop the program from immediatly exiting

	//Start everything fresh. "Blank" should never occur in the runtime logs on its own.
	for(int i=0; i <= maxStorage; i++)
	{
		storedRuntime[i] = "Blank";
		storedProc[i] = "Blank";
		storedSource[i] = "Blank";
		storedUsr[i] = "Blank";
		storedSrc[i] = "Blank";
		numRuntime[i] = 0;
		storedHardDel[i] = "Blank";
		numHardDel[i] = 0;

	}

	if(readFromFile())
	{
		cout << "Input read successfully!\n";
	}
	else
	{
		cout << "Input failed to open, shutting down.\n";
		cout << "\nEnter any letter to quit.\n";
		cin >> exit;
		return 1;
	}

	sortRuntimes();
	sortHardDels();

	if(writeToFile())
	{
		cout << "Output was successful!\n";
		cout << "\nEnter any letter to quit.\n";
		cin >> exit;
		return 0;
	}
	else
	{
		cout << "The output file could not be opened, shutting down.\n";
		cout << "\nEnter any letter to quit.\n";
		cin >> exit;
		return 0;
	}

	return 0;
}

path: tools/Runtime Condenser/Main.cpp


code:
Note: The proc name, source file, src and usr are all from the FIRST of the identical runtimes. Everything else is cropped.

Total unique runtimes: 25
Total runtimes: 767

** Runtimes **

The following runtime has occured 269 time(s).
runtime error: Cannot read null.viruses
proc name: cure (/datum/disease/proc/cure)
  source file: disease.dm,156
  usr: null
  src: Space Retrovirus (/datum/disease/dnaspread)

The following runtime has occured 199 time(s).
runtime error: Cannot read null.has_gravity
proc name: throw at (/atom/movable/proc/throw_at)
  source file: throwing.dm,194
  usr: Reese Marcotte (/mob/living/carbon/human)
  src: Reese Marcotte (/mob/living/carbon/human)

The following runtime has occured 165 time(s).
runtime error: Cannot read null.slot_flags
proc name: db click (/mob/living/carbon/human/db_click)
  source file: inventory.dm,1083
  usr: Puke Chunks (/mob/living/carbon/human)
  src: Puke Chunks (/mob/living/carbon/human)

The following runtime has occured 25 time(s).
runtime error: Cannot read null.flags
proc name: relaymove (/obj/effect/dummy/spell_jaunt/relaymove)

The following runtime has occured 24 time(s).
runtime error: Cannot read null.len
proc name: mergeRecordLists (/proc/mergeRecordLists)
  source file: helpers.dm,260
  usr: Wintermute (/mob/living/silicon/ai)
  src: null

The following runtime has occured 20 time(s).
runtime error: Cannot read null.total_volume
proc name: attackby (/obj/machinery/reagentgrinder/attackby)
  source file: Chemistry-Machinery.dm,751
  usr: Reese Marcotte (/mob/living/carbon/human)
  src: All-In-One Grinder (/obj/machinery/reagentgrinder)

The following runtime has occured 16 time(s).
runtime error: Cannot read null.broadcasting
proc name: radio menu (/mob/living/silicon/robot/proc/radio_menu)
  source file: robot.dm,857
  usr: Engineering Cyborg -432 (/mob/living/silicon/robot)
  src: Engineering Cyborg -432 (/mob/living/silicon/robot)

The following runtime has occured 7 time(s).
runtime error: unexpected stat
proc name: Stat (/mob/living/silicon/robot/Stat)
  source file: robot.dm,227
  usr: Michigan Slim (/mob/living/carbon/human)
  src: Engineering Cyborg -133 (/mob/living/silicon/robot)

The following runtime has occured 7 time(s).
runtime error: Cannot read null.current
proc name: check completion (/datum/objective/download/check_completion)
  source file: objective.dm,411
  usr: null
  src: /datum/objective/download (/datum/objective/download)

The following runtime has occured 6 time(s).
runtime error: Cannot create objects of type null.
proc name: Topic (/obj/machinery/computer/rdconsole/Topic)
  source file: rdconsole.dm,381
  usr: Matthew Hoff (/mob/living/carbon/human)
  src: Core R&D Console (/obj/machinery/computer/rdconsole/core)

The following runtime has occured 6 time(s).
runtime error: Cannot read null.fields
proc name: mergeRecordLists (/proc/mergeRecordLists)
  source file: helpers.dm,263
  usr: Wintermute (/mob/living/silicon/ai)
  src: null

The following runtime has occured 3 time(s).
runtime error: undefined variable /client/var/loc
proc name: get turf (/proc/get_turf)
  source file: helper_procs.dm,38
  usr: the ghost (/mob/dead/observer)
  src: null

The following runtime has occured 3 time(s).
runtime error: Cannot read null.nodes
proc name: merge powernets (/datum/powernet/proc/merge_powernets)
  source file: power.dm,401
  usr: Darin Keppel (/mob/living/carbon/human)
  src: /datum/powernet (/datum/powernet)

The following runtime has occured 3 time(s).
runtime error: Cannot read null.reagents
proc name: fire syringe (/obj/item/weapon/gun/syringe/proc/fire_syringe)

The following runtime has occured 2 time(s).
runtime error: undefined proc or verb /obj/machinery/space_heater/attack().

The following runtime has occured 2 time(s).
runtime error: list index out of bounds
proc name: mergeConnectedNetworksOnTurf (/obj/structure/cable/proc/mergeConnectedNetworksOnTurf)
  source file: cable.dm,520
  usr: Josue Sandford (/mob/living/carbon/human)
  src: the power cable (/obj/structure/cable)

The following runtime has occured 2 time(s).
runtime error: Cannot read null.loc
proc name: Life (/mob/living/simple_animal/corgi/Ian/Life)
  source file: corgi.dm,304
  usr: null
  src: Captain Ian (/mob/living/simple_animal/corgi/Ian)

The following runtime has occured 1 time(s).
runtime error: undefined proc or verb /obj/machinery/portable_atmospherics/canister/toxins/attack().

The following runtime has occured 1 time(s).
runtime error: undefined variable /datum/preferences/var/fields
proc name: Topic (/obj/machinery/computer/cloning/Topic)
  source file: cloning.dm,370
  usr: Logan Graves (/mob/living/carbon/human)
  src: Cloning console (/obj/machinery/computer/cloning)

The following runtime has occured 1 time(s).
runtime error: undefined variable /turf/simulated/floor/plating/var/mineral
proc name: attackby (/turf/simulated/wall/attackby)
  source file: turf.dm,600
  usr: Monte Smail (/mob/living/carbon/human)
  src: the plating (176,172,5) (/turf/simulated/floor/plating)

The following runtime has occured 1 time(s).
runtime error: Cannot read null.blood_DNA
proc name: update inv w uniform (/mob/living/carbon/human/update_inv_w_uniform)
  source file: update_icons.dm,372
  usr: null
  src: Chip Harshman (/mob/living/carbon/human)

The following runtime has occured 1 time(s).
runtime error: Cannot execute null.dropped().
proc name: drop l hand (/mob/proc/drop_l_hand)
  source file: inventory.dm,104
  usr: Jeb Stone (/mob/living/carbon/human)
  src: Jeb Stone (/mob/living/carbon/human)

The following runtime has occured 1 time(s).
runtime error: Cannot read null.w_class
proc name: attackby (/obj/item/weapon/storage/attackby)
  source file: storage.dm,187
  usr: Samuel York (/mob/living/carbon/human)
  src: the backpack (/obj/item/weapon/storage/backpack)

The following runtime has occured 1 time(s).
runtime error: Cannot read null.amount
proc name: attackby (/obj/machinery/constructable_frame/machine_frame/attackby)
  source file: constructable_frame.dm,27
  usr: Quinton Bould (/mob/living/carbon/human)
  src: the machine frame (/obj/machinery/constructable_frame/machine_frame)

The following runtime has occured 1 time(s).
runtime error: Cannot modify null.status.
proc name: death (/mob/living/silicon/robot/death)

path: tools/Runtime Condenser/Output.txt

license: mit


code:
#!/usr/bin/env python3
# Runtime Condenser, rewritten in python by tkdrg
# Only the first set of proc, file, usr and src will be kept. The rest is truncated.
# This script is valid on both python2 and python3
runtimes = dict()
infloops = dict()
harddels = dict()

def readFromFile(target='input.txt'):
	global runtimes, infloops, harddels
	f = open(target)

	for line in iter(f.readline, ''):
		if 'Infinite loop suspected' in line or 'Maximum recursion level reached' in line:
			f.readline()  # Throw away the 'If it is not an infinite loop' line
			line = f.readline()
			try:
				i = infloops[line]
				i['num'] += 1
			except KeyError:
				infloops[line] = {'file': f.readline(), 'usr': f.readline(), 'src': f.readline(), 'num': 1}

		elif 'runtime error:' in line:
			try:
				i = runtimes[line]
				i['num'] += 1
			except KeyError:
				runtimes[line] = {'proc': f.readline(), 'file': f.readline(), 'usr': f.readline(), 'src': f.readline(), 'num': 1}

		elif 'Path :' in line:
			try:
				i = harddels[line]
				i['num'] += int(f.readline().split()[2])
			except KeyError:
				harddels[line] = {'num': int(f.readline().split()[2])}

def writeToFile(target='output.txt'):
	global runtimes, infloops, harddels
	f = open(target, 'w')

	f.write('Note: The proc name, file, and usr are all from the FIRST of the identical runtimes. Everything else is cropped.\n\n')
	if len(infloops):
		f.write('Total unique infinite loops: {}\n'.format(len(infloops)))
		f.write('Total infinite loops: {}\n\n'.format(sum((i[1]['num'] for i in infloops))))

	if len(runtimes):
		f.write('Total unique runtimes: {}\n'.format(len(runtimes)))
		f.write('Total runtimes: {}\n\n'.format(sum((i[1]['num'] for i in runtimes))))

	if len(harddels):
		f.write('Total unique hard deletions: {}\n'.format(len(harddels)))
		f.write('Total hard deletions: {}\n\n'.format(sum((i[1]['num'] for i in harddels))))

	if len(infloops):
		f.write('** Infinite loops **\n')
		for t in infloops:
			i = t[1]
			f.write('The following infinite loop has occured {} time(s).\n'.format(i['num']))
			f.write(''.join([t[0], i['file'], i['usr'], i['src']]))
			f.write('\n')

	if len(runtimes):
		f.write('** Runtimes **\n')
		for t in runtimes:
			i = t[1]
			f.write('The following runtime has occured {} time(s).\n'.format(i['num']))
			f.write(''.join([t[0], i['proc'], i['file'], i['usr'], i['src']]))
			f.write('\n')

	if len(harddels):
		f.write('** Hard deletions **\n')
		for t in harddels:
			i = t[1]
			f.write('The following path has failed to GC {} time(s).\n'.format(i['num']))
			f.write('{}\n'.format(t[0]))

if __name__ == '__main__':
	readFromFile()

	runtimes = sorted(runtimes.items(), key=lambda x: x[1]['num'], reverse=True)
	infloops = sorted(infloops.items(), key=lambda x: x[1]['num'], reverse=True)
	harddels = sorted(harddels.items(), key=lambda x: x[1]['num'], reverse=True)

	writeToFile()

path: tools/Runtime Condenser/condenser.py


code:
#!/bin/bash
if [ -n "$1" ]
then
    dme=$1
else
    echo "Specify a DME to check"
    exit 1
fi

if [[ $(awk '/BEGIN_FILE_DIR/{flag=1;next}/END_FILE_DIR/{flag=0}flag' $dme | wc -l) -ne 1 ]]
then
    echo "File DIR was ticked, please untick it, see: https://tgstation13.org/phpBB/viewtopic.php?f=5&t=321 for more"
    exit 1
fi


code:
// BEGIN_INTERNALS
/*
MAP_ICON_TYPE: 0
AUTO_FILE_DIR: OFF
*/
// END_INTERNALS


code:
Uses PNGJ: https://code.google.com/p/pngj/.

For help, use "java -jar dmitool.jar help".

Requires Java 7.


code:
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'
    }
}


code:
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


code:
""" 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)


code:
@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


code:
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


code:
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.


code:
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<IconState> {
    int w, h;
    List<IconState> 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<IconState> 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<delays.length; d++) {
                        delays[d] = Float.parseFloat(delayVals[d]);
                    }
                    i++;
                } else if(lines[i].equals("\trewind = 1")) {
                    rewind = true;
                    i++;
                } else if(lines[i].startsWith("\tloop = ")) {
                    loop = Integer.parseInt(lines[i].substring("\tloop = ".length()));
                    i++;
                } else if(lines[i].startsWith("\thotspot = ")) {
                    hotspot = lines[i].substring("\thotspot = ".length());
                    i++;
                } else if(lines[i].equals("\tmovement = 1")) {
                    movement = true;
                    i++;
                } else {
                    System.out.println("Unknown line '" + lines[i] + "' in state '" + stateName + "'!");
                    i++;
                }
            }
            if(delays != null) {
                if((Main.STRICT && delays.length != frames) || delays.length < frames) {
                    throw new DMIException(null, 0, "Frames must be equal to delays (" + stateName + "; " + frames + " frames, " + delays.length + " delays)!");
                }
            }
            IconState is = new IconState(stateName, dirs, frames, null, delays, rewind, loop, hotspot, movement);
            totalImages += imagesInState;
            states.add(is);
        }
        images = states;

        PngChunkPLTE pal = (PngChunkPLTE)pngr.getChunksList().getById1("PLTE");

        isPaletted = pal != null;

        if(isPaletted) {
            if(Main.VERBOSITY > 0) System.out.println(pal.getNentries() + " palette entries");

            palette = new RGBA[pal.getNentries()];
            int[] rgb = new int[3];
            for(int q=0; q<pal.getNentries(); q++) {
                pal.getEntryRgb(q, rgb);
                palette[q] = new RGBA(rgb[0], rgb[1], rgb[2], q==0 ? 0 : 255);
            }
        } else {
            if(Main.VERBOSITY > 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<ih; y++) {
            ImageLineInt ili = (ImageLineInt)pngr.readRow();
            int[] sl = ili.getScanline();
            if(sl.length != (isPaletted ? iw : iw*4))
                throw new DMIException(null, 0, "Error processing image!");
            px[y] = sl.clone();
        }

        int statesX = iw / w;
        int statesY = ih / h;

        int x=0, y=0;
        for(IconState is: states) {
            int numImages = is.dirs * is.frames;
            Image[] img = new Image[numImages];
            for(int q=0; q<numImages; q++) {
                if(isPaletted) {
                    int[][] idat = new int[h][w];
                    for(int sy = 0; sy < h; sy++) {
                        for(int sx = 0; sx < w; sx++) {
                            idat[sy][sx] = px[y*h + sy][x*w + sx];
                        }
                    }
                    img[q] = new PalettedImage(w, h, idat, palette);
                } else {
                    RGBA[][] idat = new RGBA[h][w];
                    for(int sy = 0; sy < h; sy++) {
                        for(int sx = 0; sx < w; sx++) {
                            idat[sy][sx] = new RGBA(px[y*h + sy][x*4*w + 4*sx], px[y*h + sy][x*4*w + 4*sx + 1], px[y*h + sy][x*4*w + 4*sx + 2], px[y*h + sy][x*4*w + 4*sx + 3]);
                        }
                    }
                    img[q] = new NonPalettedImage(w, h, idat);
                }

                x++;
                if(x == statesX) {
                    x = 0;
                    y++;
                    if(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<images.size(); i++) {
            IconState ic = images.get(i);
            if(ic.name.equals(is.name)) {
                totalImages -= ic.dirs * ic.frames;
                totalImages += is.dirs * is.frames;
                images.set(i, is);
                return true;
            }
        }
        return false;
    }

    private static final int IEND = 0x49454e44;
    private static final int zTXt = 0x7a545874;
    private static final int IHDR = 0x49484452;
    private static void fixChunks(DataInputStream in, DataOutputStream out) throws IOException {
        if(Main.VERBOSITY > 0) System.out.println("Fixing PNG chunks...");
        out.writeInt(in.readInt());
        out.writeInt(in.readInt());

        Deque<PNGChunk> 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<iy; irow++) {
            ImageLineInt ili = new ImageLineInt(ii);
            int[] buf = ili.getScanline();
            for(int icol=0; icol<ix; icol++) {
                int imageX = icol / w;
                int pixelX = icol % w;

                int imageY = irow / h;
                int pixelY = irow % h;

                Image i = img[imageX][imageY];
                if(i != null) {
                    RGBA c = i.getPixel(pixelX, pixelY);
                    buf[icol*4    ] = c.r;
                    buf[icol*4 + 1] = c.g;
                    buf[icol*4 + 2] = c.b;
                    buf[icol*4 + 3] = c.a;
                } else {
                    buf[icol*4    ] = 0;
                    buf[icol*4 + 1] = 0;
                    buf[icol*4 + 2] = 0;
                    buf[icol*4 + 3] = 0;
                }
            }
            out.writeRow(ili);
        }
        out.end();

        ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
        fixChunks(new DataInputStream(bais), new DataOutputStream(os));
    }

    private String getDescriptor() {
        String s = "";
        String n = "\n";
        String q = "\"";

          s += "# BEGIN DMI\n";
          s += "version = 4.0\n";
          s += "	width = " + w + n;
          s += "	height = " + h + n;
        for(IconState is: images) {
          s += is.getDescriptorFragment();
        }
          s += "# END DMI\n";

        return s;
    }

    public void printInfo() {
        System.out.println(totalImages + " images, " + images.size() + " states, size "+w+"x"+h);
    }

    public void printStateList() {
        for(IconState s: images) {
            System.out.println(s.getInfoLine());
        }
    }

    @Override public boolean equals(Object obj) {
        if(obj == this) return true;
        if(!(obj instanceof DMI)) return false;
        DMI dmi = (DMI)obj;

        // try to find a simple difference before we dive into icon_state comparisons
        if(dmi.w != w || dmi.h != h) return false;
        if(dmi.isPaletted != isPaletted) return false;
        if(dmi.totalImages != totalImages) return false;
        if(dmi.images.size() != images.size()) return false;
        HashMap<String, IconState> myIS = new HashMap<>();
        HashMap<String, IconState> 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;
    }
}


code:
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<String, IconState> newIconStates;
    Map<String, IconStateDiff> modifiedIconStates = new HashMap<>();
    Set<String> 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<String, IconState> baseIS = new HashMap<>();
        for(IconState is: base.images) {
            baseIS.put(is.name, is);
        }

        HashMap<String, IconState> modIS = new HashMap<>();
        for(IconState is: mod.images) {
            modIS.put(is.name, is);
        }

        newIconStates = ((HashMap<String, IconState>)modIS.clone());
        for(String s: baseIS.keySet()) {
            newIconStates.remove(s);
        }

        removedIconStates = new HashSet<>();
        removedIconStates.addAll(baseIS.keySet());
        removedIconStates.removeAll(modIS.keySet());

        Set<String> 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<String> 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<String> mergeDiff(DMIDiff other, DMI conflictDMI, DMIDiff merged, String aName, String bName) {
        HashSet<String> myTouched = new HashSet<>();
        myTouched.addAll(removedIconStates);
        myTouched.addAll(newIconStates.keySet());
        myTouched.addAll(modifiedIconStates.keySet());

        HashSet<String> otherTouched = new HashSet<>();
        otherTouched.addAll(other.removedIconStates);
        otherTouched.addAll(other.newIconStates.keySet());
        otherTouched.addAll(other.modifiedIconStates.keySet());

        HashSet<String> bothTouched = (HashSet<String>)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<String> 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;
    }
}


code:
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();
    }
}


code:
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<totalFrames; i++) {
                for(int j=0; j<totalDirs; j++) {
                    img[i][j] = images[(minDir+j) + (minFrame+i) * this.dirs];
                }
            }
        }

        for(int imY=0; imY<totalDirs; imY++) {
            for(int pxY=0; pxY<h; pxY++) {
                ImageLineInt ili = new ImageLineInt(ii);
                int[] buf = ili.getScanline();
                for(int imX=0; imX<totalFrames; imX++) {
                    Image i = img[imX][imY];
                    for(int pxX=0; pxX<w; pxX++) {
                        RGBA c = i.getPixel(pxX, pxY);
                        buf[(imX*w + pxX)*4    ] = c.r;
                        buf[(imX*w + pxX)*4 + 1] = c.g;
                        buf[(imX*w + pxX)*4 + 2] = c.b;
                        buf[(imX*w + pxX)*4 + 3] = c.a;
                    }
                }
                out.writeRow(ili);
            }
        }
        out.end();
    }

    public static IconState importFromPNG(DMI dmi, InputStream inS, String name, float[] delays, boolean rewind, int loop, String hotspot, boolean movement) throws DMIException {
        int w = dmi.w;
        int h = dmi.h;

        PngReader in;
        try {
            in = new PngReader(inS);
        } catch(PngjInputException pie) {
            throw new DMIException("Bad file format!", pie);
        }
        int pxW = in.imgInfo.cols;
        int pxH = in.imgInfo.rows;
        int frames = pxW / w; //frames are read along the X axis, dirs along the Y, much like export.
        int dirs = pxH / h;

        // make sure the size is an integer multiple
        if(frames * w != pxW || frames==0) throw new DMIException("Illegal image size!");
        if(dirs * h != pxH || dirs==0) throw new DMIException("Illegal image size!");

        int[][] px = new int[pxH][];
        for(int i=0; i<pxH; i++) {
            ImageLineInt ili = (ImageLineInt)in.readRow();
            int[] sl = ili.getScanline();
            px[i] = sl.clone();
        }

        Image[] images = new Image[frames*dirs];
        for(int imageY=0; imageY<dirs; imageY++) {
            for(int imageX=0; imageX<frames; imageX++) {
                RGBA[][] pixels = new RGBA[h][w];
                for(int pixelY=0; pixelY<h; pixelY++) {
                    for(int pixelX=0; pixelX<w; pixelX++) {
                        int bY = imageY*h + pixelY;
                        int bX = imageX*4*w + 4*pixelX;
                        pixels[pixelY][pixelX] = new RGBA(px[bY][bX    ],
                                                          px[bY][bX + 1],
                                                          px[bY][bX + 2],
                                                          px[bY][bX + 3]);
                    }
                }
                images[_getIndex(imageY, imageX, dirs)] = new NonPalettedImage(w, h, pixels);
            }
        }

        //public IconState(String name, int dirs, int frames, Image[] images, float[] delays, boolean rewind, int loop, String hotspot, boolean movement) {
        return new IconState(name, dirs, frames, images, delays, rewind, loop, hotspot, movement);

    }

    //Converts a desired dir and frame to an index into the images array.
    public int getIndex(int dir, int frame) {
        return _getIndex(dir, frame, dirs);
    }

    private static int _getIndex(int dir, int frame, int totalDirs) {
        return dir + frame*totalDirs;
    }

    public void insertDir(int dir, Image[] splice) {
        int maxFrame = frames < splice.length? frames: splice.length;
        for(int frameIdx = 0; frameIdx < maxFrame; frameIdx++) {
            insertImage(dir, frameIdx, splice[frameIdx]);
        }
    }

    public void insertFrame(int frame, Image[] splice) {
        int maxDir = dirs < splice.length? dirs: splice.length;
        for(int dirIdx = 0; dirIdx < maxDir; dirIdx++) {
            insertImage(dirIdx, frame, splice[dirIdx]);
        }
    }

    public void insertImage(int dir, int frame, Image splice) {
        if(frame < 0 || frame >= 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;
    }
}


code:
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<ISAddress, Image> modifiedFrames = new HashMap<>();
    HashMap<ISAddress, Image> newFrames = new HashMap<>();
    HashSet<ISAddress> 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<maxDir; d++) {
            for(int f=0; f<maxFrame; f++) {
                if(base.dirs > 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);
    }
}


code:
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<w; i++) {
            for(int j=0; j<h; j++) {
                if(!getPixel(i, j).equals(im.getPixel(i, j))) return false;
            }
        }

        return true;
    }
}


code:
package dmitool;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.Set;

public class Main {
    public static int VERBOSITY = 0;
    public static boolean STRICT = false;
    public static final String VERSION = "v0.6 (7 Jan 2015)";

    public static final String[] dirs = new String[] {
        "S", "N", "E", "W", "SE", "SW", "NE", "NW"
    };

    public static final String helpStr =
            "help\n" +
            "\tthis text\n" +

            "version\n" +
            "\tprint version and exit\n" +

            "verify [file]\n" +
            "\tattempt to load the given file to check format\n" +

            "info [file]\n" +
            "\tprint information about [file], including a list of states\n" +

            "diff [file1] [file2]\n" +
            "\tdiff between [file1] and [file2]\n" +

            "sort [file]\n" +
            "\tsort the icon_states in [file] into ASCIIbetical order\n" +

            "merge [base] [file1] [file2] [out]\n" +
            "\tmerge [file1] and [file2]'s changes from a common ancestor [base], saving the result in [out]\n" +
            "\tconflicts will be placed in [out].conflict.dmi\n" +

            "extract [file] [state] [out] {args}\n"+
            "\textract [state] from [file] in PNG format to [out]\n" +
            "\targs specify direction and frame; input 'f' followed by a frame specifier, and/or 'd' followed by a direction specifier\n" +
            "\tframe specifier can be a single number or number-number for a range\n" +
            "\tdirection specifier can be a single direction, or direction-direction\n" +
            "\tdirection can be 0-7 or S, N, E, W, SE, SW, NE, NW (non-case-sensitive)\n" +

            "import [file] [state] [in] [options]\n" +
            "\timport a PNG image from [in] into [file], with the name [state]\n" +
            "\tinput should be in the same format given by the 'extract' command with no direction or frame arguments\n" +
            "\t(i.e. frames should be on the x-axis, and directions on the y)\n" +
            "\tpossible options:\n" +
            "\t  nodup | nd | n : if the state [state] already exists in [file], replace it instead of append\n" +
            "\t  rewind | rw | r : if there is more than one frame, the animation should be played forwards-backwards-forwards-[...]\n" +
            "\t  loop | lp | l : loop the animation infinitely; equivalent to \"loopn -1\"\n" +
            "\t  loopn N | lpn N | ln N : loop the animation N times; for infinite animations, use 'loop' or N = -1\n" +
            "\t  movement | move | mov | m : [state] should be marked as a movement state\n" +
            "\t  delays L | delay L | del L | d L : use the list L as a comma-separated list of delays (e.g. '1,1,2,2,1')\n" +
            "\t  hotspot H | hs H | h H : use H as the hotspot for this state\n" +
            "\t  direction D | dir D : replaces D with the image from [in], instead of the entire state. D can be 0-7 or S, N, E, etc. If the state does not already exist, this is ignored\n" +
            "";

    public static void main(String[] args) throws FileNotFoundException, IOException, DMIException {
        Deque<String> 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<String> 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<delaysSplit.length; i++) {
                                    try {
                                        delays[i] = Integer.parseInt(delaysSplit[i]);
                                    } catch(NumberFormatException nfe) {
                                        System.out.println("Illegal number '" + delaysSplit[i] + "' as argument to '" + s + "'!");
                                        return;
                                    }
                                }
                            } else {
                                System.out.println("Argument '" + s + "' requires a list of delays (in the format 'a,b,c,d,[...]') following it!");
                                return;
                            }
                            break;
                        case "hotspot":
                        case "hs":
                        case "h":
                            if(!argq.isEmpty()) {
                                hotspot = argq.pollFirst();
                            } else {
                                System.out.println("Argument '" + s + "' requires a hotspot string following it!");
                                return;
                            }
                            break;
                        case "dir":
                        case "direction":
                            if(!argq.isEmpty()) {
                                replaceDir = argq.pollFirst();
                            } else {
                                System.out.println("Argument '" + s + "' requires a direction argument following it!");
                                return;
                            }
                            break;
                        case "f":
                        case "frame":
                            if(!argq.isEmpty()) {
                                replaceFrame = argq.pollFirst();
                            } else {
                                System.out.println("Argument '" + s + "' requires a frame argument following it!");
                                return;
                            }
                            break;
                        default:
                            System.out.println("Unknown import argument '" + s + "', ignoring.");
                            break;
                    }
                }

                if(VERBOSITY >= 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<dirs.length && q < is.dirs; q++) {
                if(dirs[q].equalsIgnoreCase(s)) {
                    return q;
                }
            }
            String dSummary = "";
            for(int i=0; i<is.dirs; i++) {
                dSummary += ", " + dirs[i];
            }
            dSummary = dSummary.substring(2);
            System.out.println("Unknown or non-existent direction '" + s + "'!");
            System.out.println("Valid range: [0, "+(is.dirs-1)+"], or " + dSummary);
            return -1;
        }
    }

    static int parseFrame(String s, IconState is) {
        try {
            int i = Integer.parseInt(s);
            if(0 <= i && i < is.frames) {
                return i;
            } else {
                System.out.println("Frame not in valid range [0, "+(is.frames-1)+"]!");
                return -1;
            }
        } catch(NumberFormatException nfe) {
            System.out.println("Failed to parse frame number: '" + s + "'!");
            return -1;
        }
    }

    static DMI doDMILoad(String file) {
        try {
            DMI dmi = new DMI(file);
            return dmi;
        } catch(DMIException dmie) {
            System.out.println("Failed to load " + file + ": " + dmie.getMessage());
        } catch(FileNotFoundException fnfe) {
            System.out.println("File not found: " + file);
        }
        System.exit(3);
        return null;
    }
}


code:
package dmitool;

public class NonPalettedImage extends Image {
    RGBA[][] pixels;

    public NonPalettedImage(int w, int h, RGBA[][] pixels) {
        super(w, h);
        this.pixels = pixels;
    }

    RGBA getPixel(int x, int y) {
        return pixels[y][x];
    }
}


code:
package dmitool;

import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;

public class PNGChunk {
    int len;
    int type;
    byte[] b;
    int crc;

    public PNGChunk(DataInputStream in) throws IOException {
        len = in.readInt();
        type = in.readInt();
        b = new byte[len];
        in.read(b);
        crc = in.readInt();
    }

    void write(DataOutputStream out) throws IOException {
        out.writeInt(len);
        out.writeInt(type);
        out.write(b);
        out.writeInt(crc);
    }
}


code:
package dmitool;

public class PalettedImage extends Image {
    int[][] pixels;
    RGBA[] pal;

    public PalettedImage(int w, int h, int[][] pixels, RGBA[] palette) {
        super(w, h);
        this.pixels = pixels;
        this.pal = palette;
    }

    RGBA getPixel(int x, int y) {
        return pal[pixels[y][x]];
    }
}


code:
package dmitool;

public class RGBA {
    int r, g, b, a;

    public RGBA(int r, int g, int b, int a) {
        this.r = r;
        this.g = g;
        this.b = b;
        this.a = a;
    }

    @Override
    public String toString() {
        String s = Long.toString(toRGBA8888());
        while(s.length() < 8)
            s = "0" + s;
        return "#" + s;
    }

    @Override public boolean equals(Object obj) {
        if(obj == this) return true;
        if(!(obj instanceof RGBA)) return false;

        RGBA o = (RGBA) obj;

        return r==o.r && g==o.g && b==o.b && a==o.a;
    }

    public long toRGBA8888() {
        return (r<<24) | (g<<16) | (b<<8) | a;
    }
}


code:
import sys

# .dmm format converter, by RemieRichards
# Version 2.0
# Converts the internal structure of a .dmm file to a syntax
# that git can better handle conflicts-wise, it's also fairly human readable!
# Processes Boxstation (tgstation.2.1.3) almost instantly

def convert_map(map_file):
        #CHECK FOR PREVIOUS CONVERSION
        with open(map_file, "r") as conversion_candidate:
                header = conversion_candidate.readline()
                if header.find("//MAP CONVERTED BY dmm2tgm.py THIS HEADER COMMENT PREVENTS RECONVERSION, DO NOT REMOVE") != -1:
                        sys.exit()
                        return

        #ACTUAL CONVERSION
        with open(map_file, "r+") as unconverted_map:
                characters = unconverted_map.read()
                converted_map = ""
                in_object_block = False #()
                in_variable_block = False #{}
                in_quote_block = False #''
                in_double_quote_block = False #""
                for char in characters:
                        if not in_quote_block: #Checking for things like "Flashbangs (Warning!)" Because we only care about ({'";, that are used as byond syntax, not strings
                                if not in_double_quote_block:
                                        if not in_variable_block:
                                                if char == "(":
                                                        in_object_block = True
                                                        char = char + "\n"
                                                if char == ")":
                                                        in_object_block = False
                                                if char == ",":
                                                        char = char + "\n"

                                        if char == "{":
                                                in_variable_block = True
                                                if in_object_block:
                                                        char = char + "\n\t"
                                        if char == "}":
                                                in_variable_block = False
                                                if in_object_block:
                                                        char = "\n\t" + char

                                        if char == ";":
                                                char = char + "\n\t"

                                if char == "\"":
                                        if in_double_quote_block:
                                                in_double_quote_block = False
                                        else:
                                                in_double_quote_block = True

                        if char == "'":
                                if not in_double_quote_block:
                                        if in_quote_block:
                                                in_quote_block = False
                                        else:
                                                in_quote_block = True

                        converted_map = converted_map + char

        #OVERWRITE MAP FILE WITH CONVERTED MAP STRING
        with open(map_file, "r+") as final_converted_map:
                final_converted_map.write("//MAP CONVERTED BY dmm2tgm.py THIS HEADER COMMENT PREVENTS RECONVERSION, DO NOT REMOVE \n")
                final_converted_map.write(converted_map)

        sys.exit()

if sys.argv[1]: #Run like dmm2tgm.py "folder/folder/a_map.dmm"
        convert_map(sys.argv[1])


code:
#!/usr/bin/env python

import re, os, sys, fnmatch

# Regex pattern to extract the directory path in a #define FILE_DIR
filedir_pattern = re.compile(r'^#define\s*FILE_DIR\s*"(.*?)"')

# Regex pattern to extract any single quoted piece of text. This can also
# match single quoted strings inside of double quotes, which is part of a
# regular text string and should not be replaced. The replacement function
# however will any match that doesn't appear to be a filename so these
# extra matches should not be a problem.
rename_pattern = re.compile(r"'(.+?)'")

# Only filenames matching this pattern will have their resources renamed
source_pattern = re.compile(r"^.*?\.(dm|dmm)$")

# Open the .dme file and return a list of all FILE_DIR paths in it
def read_filedirs(filename):
    result = []
    dme_file = file(filename, "rt")

    # Read each line from the file and check for regex pattern match
    for row in dme_file:
        match = filedir_pattern.match(row)
        if match:
            result.append(match.group(1))

    dme_file.close()
    return result

# Search through a list of directories, and build a dictionary which
# maps every file to its full pathname (relative to the .dme file)
# If the same filename appears in more than one directory, the earlier
# directory in the list takes preference.
def index_files(file_dirs):
    result = {}

    # Reverse the directory list so the earlier directories take precedence
    # by replacing the previously indexed file of the same name
    for directory in reversed(file_dirs):
        for name in os.listdir(directory):
            # Replace backslash path separators on Windows with forward slash
            # Force "name" to lowercase when used as a key since BYOND resource
            # names are case insensitive, even on Linux.
            if name.find(".") == -1:
                continue
            result[name.lower()] = directory.replace('\\', '/') + '/' + name

    return result

# Recursively search for every .dm/.dmm file in the .dme file directory. For
# each file, search it for any resource names in single quotes, and replace
# them with the full path previously found by index_files()
def rewrite_sources(resources):
    # Create a closure for the regex replacement function to capture the
    # resources dictionary which can't be passed directly to this function
    def replace_func(name):
        key = name.group(1).lower()
        if key in resources:
            replacement = resources[key]
        else:
            replacement = name.group(1)
        return "'" + replacement + "'"

    # Search recursively for all .dm and .dmm files
    for (dirpath, dirs, files) in os.walk("."):
        for name in files:
            if source_pattern.match(name):
                path = dirpath + '/' + name
                source_file = file(path, "rt")
                output_file = file(path + ".tmp", "wt")

                # Read file one line at a time and perform replacement of all
                # single quoted resource names with the fullpath to that resource
                # file. Write the updated text back out to a temporary file.
                for row in source_file:
                    row = rename_pattern.sub(replace_func, row)
                    output_file.write(row)

                output_file.close()
                source_file.close()

                # Delete original source file and replace with the temporary
                # output. On Windows, an atomic rename() operation is not
                # possible like it is under POSIX.
                os.remove(path)
                os.rename(path + ".tmp", path)

dirs = read_filedirs("tgstation.dme");
resources = index_files(dirs)
rewrite_sources(resources)