Downloads containing xmasAccessories - bk.asc

Downloads
Name Author Game Mode Rating
TSF with JJ2+ Only: Characters Cranky Mutator N/A Download file

File preview

namespace XmasHats
{
	bool LOAD_LEDGES_J2A = false;
	const bool DEBUG_YOSHI_ON = false;
	bool EDITING_MODE = false;
	bool SHOW_HAT_INSTRUCTIONS = true;
	array<int> hats(32,0);
	const string Filename2 = "hatSave.asdat";
	const int FRAMES_PER_CHARACTER = 858;
	const int MAX_ANIM_SETS = 256;
	const int ANIMS_PER_SET = 90;
	const int HANG_LEDGE_FRAME_INDEX_START = FRAMES_PER_CHARACTER - 10;
	int HANG_LEDGE_RUNTIME_ACCESSORY_Y_OFFSET_JAZZ = 10;
	int HANG_LEDGE_RUNTIME_ACCESSORY_Y_OFFSET_SPAZ = 6;
	int DEVAN_RUNTIME_ACCESSORY_X_OFFSET = 10;
	int DEVAN_RUNTIME_ACCESSORY_Y_OFFSET = -16;

	// Normal Yoshi only: hat X correction for running animation 56.
	// Change -5 here later if you need to adjust it.
	const int NORMAL_YOSHI_RUNNING_56_HAT_X_OFFSET = -5;
	const int ACCESSORY_COLOR_GROUP_RED = 0;
	const int ACCESSORY_COLOR_GROUP_YELLOW = 1;
	const int ACCESSORY_COLOR_GROUP_BLACK = 2;
	const int ACCESSORY_COLOR_GROUP_BLUE = 3;
	const int ACCESSORY_COLOR_GROUP_GREEN = 4;
	const int ACCESSORY_COLOR_GROUP_COUNT = 5;
	array<int> HAT_COLOR_OFFSET_RED_COLORS = {24, 25, 26, 27, 28, 29, 30, 31};
	array<int> HAT_COLOR_OFFSET_YELLOW_COLORS = {40, 41, 42, 43, 44, 45, 46, 47};
	array<int> HAT_COLOR_OFFSET_BLACK_COLORS = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 23};
	array<int> HAT_COLOR_OFFSET_BLUE_COLORS = {32, 33, 34, 35, 36, 37, 38, 39};
	array<int> HAT_COLOR_OFFSET_GREEN_COLORS = {16, 17, 18, 19, 20, 21, 22};
	int HAT_COLOR_OFFSET_BLACK_PALSHIFT_BASE = 24;
	const bool USE_PRESET_COORDINATES_HAT = true;
	const bool USE_PRESET_COORDINATES_SCARFS = false;
	const bool USE_PRESET_COORDINATES_GLASSES = false;
	int characterSlotCount = 32;
	const float accessoryBaseMultiplier = 1;
	enum AccessoryCategory
	{
		HATS = 0,
		SCARFS = 1,
		GLASSES = 2
	}
	array<string> AccessoryCoordinateFilenames = {"xmasHats.asdat", "xmasScarfs.asdat", "xmasGlasses.asdat"};
	array<string> AccessoryCategoryNames = {"Hats", "Scarfs", "Glasses"};
	int animation = ANIM::JAZZ;
	int character = CHAR::JAZZ;

	array<int> adjustX(0, 0);
	array<int> adjustY(0, 0);
	array<int> rotation(0, 0);
	array<int> using(0, 0);
	array<float> scaleX(0, 0);
	array<float> scaleY(0, 0);
	array<int> edited(0, 0);
	array<int> categoryAdjustX(0, 0);
	array<int> categoryAdjustY(0, 0);
	array<int> categoryRotation(0, 0);
	array<int> categoryUsing(0, 0);
	array<float> categoryScaleX(0, 0);
	array<float> categoryScaleY(0, 0);
	array<int> categoryEdited(0, 0);
	array<int> accessoryFrames(96, 0);
	array<int> accessoryColorOffsets(96 * ACCESSORY_COLOR_GROUP_COUNT, 0);
	array<bool> noAccessory(96, false);
	array<bool> nohat(32, false);
	bool transmitServerHats = false;
	bool hatAnimLoaded = false;
	bool accessoryColorMaskAnimLoaded = false;
	bool ledgesJazzLoaded = false;
	bool ledgesSpazLoaded = false;
	bool ledgesSingleSetMode = false;

	uint empty = 0;
	uint accessoryColorMaskSet = 0;
	uint ledgesJazzSet = 0;
	uint ledgesSpazSet = 0;
	uint nr = 0;
	int hatFrameCount = 1;
	int editCategory = AccessoryCategory::HATS;
	bool currentCategoryLoaded = false;
	array<int> accessoryColorMaskFrameStart(3, 0);
	int accessoryColorMaskFramesPerGroup = 0;
	
	int characterJ2AIndex(int charSlot)
	{
		if(charSlot == 0) return 1;
		if(charSlot == 7) return 2;
		if(charSlot == 8) return 3;
		if(charSlot == 9) return 4;
		if(charSlot == 10) return 5;
		if(charSlot == 11) return 6;
		if(charSlot == 12) return 7;
		if(charSlot == 13) return 8;
		return -1;
	}
	
	int positionArrayLength()
	{
		return FRAMES_PER_CHARACTER * characterSlotCount + 1;
	}
	void resizePositionArrays(int newCharacterSlotCount)
	{
		if(newCharacterSlotCount < 5) newCharacterSlotCount = 5;
		characterSlotCount = newCharacterSlotCount;
		int length = positionArrayLength();
		uint oldLength = scaleX.length;
		uint oldCategoryLength = categoryScaleX.length;
		adjustX.resize(length);
		adjustY.resize(length);
		rotation.resize(length);
		using.resize(length);
		scaleX.resize(length);
		scaleY.resize(length);
		edited.resize(length);
		int categoryLength = int(AccessoryCategoryNames.length) * length;
		categoryAdjustX.resize(categoryLength);
		categoryAdjustY.resize(categoryLength);
		categoryRotation.resize(categoryLength);
		categoryUsing.resize(categoryLength);
		categoryScaleX.resize(categoryLength);
		categoryScaleY.resize(categoryLength);
		categoryEdited.resize(categoryLength);
		for(uint i = oldLength; i < scaleX.length; i++){
			scaleX[i] = 1;
			scaleY[i] = 1;
		}
		for(uint i = oldCategoryLength; i < categoryScaleX.length; i++){
			categoryScaleX[i] = 1;
			categoryScaleY[i] = 1;
		}
	}
	
	void onLevelLoad(){
		resizePositionArrays(characterSlotCount);
		//find an unused slot in ANIM::CUSTOM
		while (jjAnimSets[ANIM::CUSTOM[empty]].firstAnim != 0)
			++empty;
		jjANIMSET@ anim = jjAnimSets[ANIM::CUSTOM[empty]];
		anim.load(0, "XmasHats.j2a");
		hatAnimLoaded = true;
		hatFrameCount = accessoryFrameCount(AccessoryCategory::HATS);
		loadLedgesEditorAnimations();
		buildRabbit123PackedFrameCache();
		
		for(int i=0; i<78; i++){
			nr += jjAnimations[jjAnimSets[animation].firstAnim + anim_id].frameCount;
		}
		
		loadHatPreferences();
		readAllCategoryFiles();
	}

	uint nextFreeCustomAnimSet(uint start)
	{
		while(jjAnimSets[ANIM::CUSTOM[start]].firstAnim != 0)
			start++;
		return start;
	}

	void loadLedgesEditorAnimations()
	{
		ledgesJazzLoaded = false;
		ledgesSpazLoaded = false;
		ledgesSingleSetMode = false;
		if(!LOAD_LEDGES_J2A) return;
		ledgesJazzSet = nextFreeCustomAnimSet(accessoryColorMaskAnimLoaded ? accessoryColorMaskSet + 1 : empty + 1);
		jjAnimSets[ANIM::CUSTOM[ledgesJazzSet]].load(0, "Ledges.j2a");
		ledgesJazzLoaded = jjAnimSets[ANIM::CUSTOM[ledgesJazzSet]].firstAnim != 0;
		ledgesSpazSet = nextFreeCustomAnimSet(ledgesJazzSet + 1);
		jjAnimSets[ANIM::CUSTOM[ledgesSpazSet]].load(1, "Ledges.j2a");
		ledgesSpazLoaded = jjAnimSets[ANIM::CUSTOM[ledgesSpazSet]].firstAnim != 0;
		if(ledgesJazzLoaded && !ledgesSpazLoaded)
		{
			int firstAnim = jjAnimSets[ANIM::CUSTOM[ledgesJazzSet]].firstAnim;
			ledgesSingleSetMode = jjAnimations[firstAnim + 1].frameCount > 0;
		}
	}

	void sendHatState(int playerID)
	{
		if(playerID < 0 || playerID >= 32) return;
		for(int category = 0; category < int(AccessoryCategoryNames.length); category++){
			int index = accessoryPlayerIndex(category, playerID);
			jjSTREAM mySTREAM;
			mySTREAM.push("!accessory " + category + " " + accessoryFrames[index] + " " + (noAccessory[index] ? 1 : 0) + " " + playerID);
			jjSendPacket(mySTREAM);
			for(int colorGroup = 0; colorGroup < ACCESSORY_COLOR_GROUP_COUNT; colorGroup++){
				mySTREAM.clear();
				mySTREAM.push("!accessoryColor " + category + " " + colorGroup + " " + getAccessoryColorOffset(category, playerID, colorGroup) + " " + playerID);
				jjSendPacket(mySTREAM);
			}
			if(jjIsServer){
				mySTREAM.clear();
				mySTREAM.push("!accessories " + category + " " + accessoryFrames[index] + " " + (noAccessory[index] ? 1 : 0) + " " + playerID);
				jjSendPacket(mySTREAM);
				for(int colorGroup = 0; colorGroup < ACCESSORY_COLOR_GROUP_COUNT; colorGroup++){
					mySTREAM.clear();
					mySTREAM.push("!accessoryColors " + category + " " + colorGroup + " " + getAccessoryColorOffset(category, playerID, colorGroup) + " " + playerID);
					jjSendPacket(mySTREAM);
				}
			}
		}
	}

	void sendHatStateToClient(int subjectID, int toClientID)
	{
		if(subjectID < 0 || subjectID >= 32 || toClientID <= 0) return;
		for(int category = 0; category < int(AccessoryCategoryNames.length); category++){
			int index = accessoryPlayerIndex(category, subjectID);
			jjSTREAM packet;
			packet.push("!accessories " + category + " " + accessoryFrames[index] + " " + (noAccessory[index] ? 1 : 0) + " " + subjectID);
			jjSendPacket(packet, toClientID);
			for(int colorGroup = 0; colorGroup < ACCESSORY_COLOR_GROUP_COUNT; colorGroup++){
				packet.clear();
				packet.push("!accessoryColors " + category + " " + colorGroup + " " + getAccessoryColorOffset(category, subjectID, colorGroup) + " " + subjectID);
				jjSendPacket(packet, toClientID);
			}
		}
	}

	void transmitAllHatsToClient(int toClientID)
	{
		if(toClientID <= 0) return;
		for(int subjectID = 0; subjectID < 32; subjectID++){
			if(jjPlayers[subjectID].isActive)
				sendHatStateToClient(subjectID, toClientID);
		}
	}

	void transmitServerHatsRequest()
	{
		if(transmitServerHats || jjIsServer) return;
		jjSTREAM myStream;
		myStream.push("!allHats");
		jjSendPacket(myStream);
		transmitServerHats = true;
	}

	void saveHatPreferences()
	{
		jjSTREAM saveFile(Filename2);
		saveFile.clear();
		for(int i = 0; i < jjLocalPlayerCount; i++){
			int playerID = jjLocalPlayers[i].playerID;
			saveFile.write((i + 1) + " " + hats[playerID] + " " + (nohat[playerID] ? 1 : 0));
			for(int category = 1; category < int(AccessoryCategoryNames.length); category++){
				int index = accessoryPlayerIndex(category, playerID);
				saveFile.write(" " + accessoryFrames[index] + " " + (noAccessory[index] ? 1 : 0));
			}
			for(int category = 0; category < int(AccessoryCategoryNames.length); category++)
				for(int colorGroup = 0; colorGroup < ACCESSORY_COLOR_GROUP_COUNT; colorGroup++)
					saveFile.write(" " + getAccessoryColorOffset(category, playerID, colorGroup));
			saveFile.write("\n");
		}
		saveFile.save(Filename2);
	}

	void loadHatPreferences()
	{
		for(int i = 0; i < jjLocalPlayerCount; i++){
			int playerID = jjLocalPlayers[i].playerID;
			hats[playerID] = 0;
			nohat[playerID] = false;
			for(int category = 0; category < int(AccessoryCategoryNames.length); category++){
				int index = accessoryPlayerIndex(category, playerID);
				accessoryFrames[index] = 0;
				noAccessory[index] = category == AccessoryCategory::HATS ? false : true;
				for(int colorGroup = 0; colorGroup < ACCESSORY_COLOR_GROUP_COUNT; colorGroup++)
					setAccessoryColorOffset(category, playerID, colorGroup, 0);
			}
		}

		jjSTREAM loadFile(Filename2);
		if(!loadFile.isEmpty()){
			bool readLegacy = false;
			while(!loadFile.isEmpty()){
				string line = "";
				loadFile.getLine(line, '\n');
				array<string> string_parts = line.split(" ");
				if(string_parts.length >= 3 && string_parts[2] != ""){
					int localIndex = parseInt(string_parts[0]) - 1;
					if(localIndex >= 0 && localIndex < jjLocalPlayerCount){
						int playerID = jjLocalPlayers[localIndex].playerID;
						setAccessoryFrame(AccessoryCategory::HATS, playerID, parseInt(string_parts[1]));
						if(parseInt(string_parts[2]) == 1) removeAccessory(AccessoryCategory::HATS, playerID);
						for(int category = 1; category < int(AccessoryCategoryNames.length); category++){
							int partIndex = 3 + (category - 1) * 2;
							if(int(string_parts.length) > partIndex + 1){
								setAccessoryFrame(category, playerID, parseInt(string_parts[partIndex]));
								if(parseInt(string_parts[partIndex + 1]) == 1) removeAccessory(category, playerID);
							}
						}
						int colorStartIndex = 7;
						int expandedColorCount = int(AccessoryCategoryNames.length) * ACCESSORY_COLOR_GROUP_COUNT;
						if(int(string_parts.length) >= colorStartIndex + expandedColorCount)
						{
							for(int category = 0; category < int(AccessoryCategoryNames.length); category++)
								for(int colorGroup = 0; colorGroup < ACCESSORY_COLOR_GROUP_COUNT; colorGroup++){
									int partIndex = colorStartIndex + category * ACCESSORY_COLOR_GROUP_COUNT + colorGroup;
									setAccessoryColorOffset(category, playerID, colorGroup, parseInt(string_parts[partIndex]));
								}
						}
						else
						{
							for(int category = 0; category < int(AccessoryCategoryNames.length); category++){
								int partIndex = colorStartIndex + category;
								if(int(string_parts.length) > partIndex)
									setAccessoryColorOffset(category, playerID, ACCESSORY_COLOR_GROUP_RED, parseInt(string_parts[partIndex]));
							}
						}
					}
				}else if(!readLegacy && string_parts.length >= 1 && jjLocalPlayerCount > 0){
					int playerID = jjLocalPlayers[0].playerID;
					setAccessoryFrame(AccessoryCategory::HATS, playerID, parseInt(string_parts[0]));
					if(string_parts.length > 1 && parseInt(string_parts[1]) == 1) removeAccessory(AccessoryCategory::HATS, playerID);
					readLegacy = true;
				}
			}
		}

		if(jjLocalPlayerCount > 0)
			use = hats[jjLocalPlayers[0].playerID];
		for(int i = 0; i < jjLocalPlayerCount; i++)
			sendHatState(jjLocalPlayers[i].playerID);
		transmitServerHatsRequest();
	}

	int chosen_frame = 0, adjX = 0, adjY = 0, rot = 0, anim_id = 0;
	int additionalAccessory = -1;
	float adjScaleX = 1, adjScaleY = 1;
	uint frame_id = 0, use = 0;
	bool pressedUp = false, pressedDown = false, pressed = false, pressedJump = false, pressedKey = false;
	bool hatMouseMode = false, hatMouseWasDown = false, hatEditFresh = false;
	int hatEditField = -1, hatEditLastKey = -1;
	string hatEditValue = "";
	uint choice = 0;
	DbWindow hatEditorWindow;
	DbResult hatEditorResult;
	const int HAT_ACTION_CATEGORY = 1000;
	const int HAT_ACTION_INCREMENT = 1001;
	const int HAT_ACTION_ANIM = 1002;
	const int HAT_ACTION_FRAME = 1003;
	const int HAT_ACTION_CHAR = 1004;
	const int HAT_ACTION_POS_X = 1005;
	const int HAT_ACTION_POS_Y = 1006;
	const int HAT_ACTION_ROTATION = 1007;
	const int HAT_ACTION_SCALE_X = 1008;
	const int HAT_ACTION_SCALE_Y = 1009;
	const int HAT_ACTION_MOUSE = 1010;
	const int HAT_ACTION_EDIT_FIELD = 1011;
	const int HAT_ACTION_ADDITIONAL_HAT = 1012;
	const int HAT_ACTION_ROT_INCREMENT = 1013;

	int selectedCharacterSlot = 1;
	string currentCoordinateFilename()
	{
		if(editCategory < 0 || editCategory >= int(AccessoryCoordinateFilenames.length))
			return AccessoryCoordinateFilenames[AccessoryCategory::HATS];
		return AccessoryCoordinateFilenames[editCategory];
	}
	string currentCategoryName()
	{
		if(editCategory < 0 || editCategory >= int(AccessoryCategoryNames.length))
			return AccessoryCategoryNames[AccessoryCategory::HATS];
		return AccessoryCategoryNames[editCategory];
	}
	int accessoryFrameCount(int category)
	{
		if(!hatAnimLoaded || category < 0 || category >= int(AccessoryCategoryNames.length))
			return 1;
		int firstAnim = jjAnimSets[ANIM::CUSTOM[empty]].firstAnim;
		int count = jjAnimations[firstAnim + category].frameCount;
		return count > 0 ? count : 1;
	}
	bool accessoryColorListContains(uint8 color, const array<int> &in colors)
	{
		for(uint i = 0; i < colors.length; i++)
			if(int(color) == colors[i])
				return true;
		return false;
	}
	int accessoryColorListIndex(uint8 color, const array<int> &in colors)
	{
		for(uint i = 0; i < colors.length; i++)
			if(int(color) == colors[i])
				return int(i);
		return -1;
	}
	bool isAccessoryColorGroupPixel(uint8 color, int colorGroup)
	{
		if(color == 0) return false;
		if(colorGroup == ACCESSORY_COLOR_GROUP_RED) return accessoryColorListContains(color, HAT_COLOR_OFFSET_RED_COLORS);
		if(colorGroup == ACCESSORY_COLOR_GROUP_YELLOW) return accessoryColorListContains(color, HAT_COLOR_OFFSET_YELLOW_COLORS);
		if(colorGroup == ACCESSORY_COLOR_GROUP_BLACK) return accessoryColorListContains(color, HAT_COLOR_OFFSET_BLACK_COLORS);
		if(colorGroup == ACCESSORY_COLOR_GROUP_BLUE) return accessoryColorListContains(color, HAT_COLOR_OFFSET_BLUE_COLORS);
		if(colorGroup == ACCESSORY_COLOR_GROUP_GREEN) return accessoryColorListContains(color, HAT_COLOR_OFFSET_GREEN_COLORS);
		return false;
	}
	uint8 accessoryPalshiftMaskColor(uint8 color, int colorGroup)
	{
		if(colorGroup != ACCESSORY_COLOR_GROUP_BLACK)
			return color;
		int index = accessoryColorListIndex(color, HAT_COLOR_OFFSET_BLACK_COLORS);
		if(index < 0)
			return color;
		int count = int(HAT_COLOR_OFFSET_BLACK_COLORS.length);
		int shade = count <= 1 ? 0 : (index * 6) / (count - 1);
		int mapped = HAT_COLOR_OFFSET_BLACK_PALSHIFT_BASE + shade;
		if(mapped < 1) mapped = 1;
		if(mapped > 255) mapped = 255;
		return uint8(mapped);
	}
	void copyAccessoryFrameMetadata(int sourceFrameID, int targetFrameID)
	{
		jjANIMFRAME@ sourceFrame = jjAnimFrames[sourceFrameID];
		jjANIMFRAME@ targetFrame = jjAnimFrames[targetFrameID];
		targetFrame.hotSpotX = sourceFrame.hotSpotX;
		targetFrame.hotSpotY = sourceFrame.hotSpotY;
		targetFrame.gunSpotX = sourceFrame.gunSpotX;
		targetFrame.gunSpotY = sourceFrame.gunSpotY;
		targetFrame.coldSpotX = sourceFrame.coldSpotX;
		targetFrame.coldSpotY = sourceFrame.coldSpotY;
		targetFrame.transparent = sourceFrame.transparent;
	}
	void buildAccessoryColorMaskAnimation()
	{
		accessoryColorMaskAnimLoaded = false;
		accessoryColorMaskFramesPerGroup = 0;
		if(!hatAnimLoaded) return;

		for(int category = 0; category < int(AccessoryCategoryNames.length); category++){
			accessoryColorMaskFrameStart[category] = accessoryColorMaskFramesPerGroup;
			accessoryColorMaskFramesPerGroup += accessoryFrameCount(category);
		}
		if(accessoryColorMaskFramesPerGroup <= 0) return;

		accessoryColorMaskSet = nextFreeCustomAnimSet(empty + 1);
		array<uint> animSizes(1);
		animSizes[0] = uint(accessoryColorMaskFramesPerGroup * ACCESSORY_COLOR_GROUP_COUNT);
		jjAnimSets[ANIM::CUSTOM[accessoryColorMaskSet]].allocate(animSizes);

		int sourceFirstAnim = jjAnimSets[ANIM::CUSTOM[empty]].firstAnim;
		int maskFirstFrame = jjAnimations[jjAnimSets[ANIM::CUSTOM[accessoryColorMaskSet]].firstAnim].firstFrame;
		for(int colorGroup = 0; colorGroup < ACCESSORY_COLOR_GROUP_COUNT; colorGroup++){
			int groupFrameOffset = colorGroup * accessoryColorMaskFramesPerGroup;
			for(int category = 0; category < int(AccessoryCategoryNames.length); category++){
				int frameCount = accessoryFrameCount(category);
				int sourceFirstFrame = jjAnimations[sourceFirstAnim + category].firstFrame;
				for(int frame = 0; frame < frameCount; frame++){
					int sourceFrameID = sourceFirstFrame + frame;
					int maskFrameID = maskFirstFrame + groupFrameOffset + accessoryColorMaskFrameStart[category] + frame;
					jjPIXELMAP image(jjAnimFrames[sourceFrameID]);
					for(int x = image.width - 1; x >= 0; x--)
						for(int y = image.height - 1; y >= 0; y--)
							if(isAccessoryColorGroupPixel(image[x,y], colorGroup))
								image[x,y] = accessoryPalshiftMaskColor(image[x,y], colorGroup);
							else
								image[x,y] = 0;
					image.save(jjAnimFrames[maskFrameID]);
					copyAccessoryFrameMetadata(sourceFrameID, maskFrameID);
				}
			}
		}
		accessoryColorMaskAnimLoaded = true;
	}
	void ensureAccessoryColorMaskAnimation()
	{
		if(!accessoryColorMaskAnimLoaded)
			buildAccessoryColorMaskAnimation();
	}
	int accessoryColorMaskFrame(int colorGroup, int category, int frame)
	{
		if(!accessoryColorMaskAnimLoaded
		|| colorGroup < 0 || colorGroup >= ACCESSORY_COLOR_GROUP_COUNT
		|| category < 0 || category >= int(AccessoryCategoryNames.length))
			return -1;
		int count = accessoryFrameCount(category);
		if(count <= 0 || accessoryColorMaskFramesPerGroup <= 0) return -1;
		frame %= count;
		if(frame < 0) frame += count;
		return colorGroup * accessoryColorMaskFramesPerGroup + accessoryColorMaskFrameStart[category] + frame;
	}
	void resetPositionArrays()
	{
		for(uint i = 0; i < adjustX.length; i++){
			adjustX[i] = 0;
			adjustY[i] = 0;
			rotation[i] = 0;
			using[i] = 0;
			scaleX[i] = 1;
			scaleY[i] = 1;
			edited[i] = 0;
		}
		adjX = 0;
		adjY = 0;
		rot = 0;
		adjScaleX = 1;
		adjScaleY = 1;
		chosen_frame = getPositionIndex(coordinateSlotForEditor(selectedCharacterSlot), 0);
	}
	void resetCategoryPositionArrays(int category)
	{
		for(int i = 0; i < positionArrayLength(); i++){
			int index = category * positionArrayLength() + i;
			categoryAdjustX[index] = 0;
			categoryAdjustY[index] = 0;
			categoryRotation[index] = 0;
			categoryUsing[index] = 0;
			categoryScaleX[index] = 1;
			categoryScaleY[index] = 1;
			categoryEdited[index] = 0;
		}
	}
	void copyCategoryToCurrent(int category)
	{
		for(int i = 0; i < positionArrayLength(); i++){
			int index = category * positionArrayLength() + i;
			adjustX[i] = categoryAdjustX[index];
			adjustY[i] = categoryAdjustY[index];
			rotation[i] = categoryRotation[index];
			using[i] = categoryUsing[index];
			scaleX[i] = categoryScaleX[index];
			scaleY[i] = categoryScaleY[index];
			edited[i] = categoryEdited[index];
		}
	}
	void copyCurrentToCategory(int category)
	{
		for(int i = 0; i < positionArrayLength(); i++){
			int index = category * positionArrayLength() + i;
			categoryAdjustX[index] = adjustX[i];
			categoryAdjustY[index] = adjustY[i];
			categoryRotation[index] = rotation[i];
			categoryUsing[index] = using[i];
			categoryScaleX[index] = scaleX[i];
			categoryScaleY[index] = scaleY[i];
			categoryEdited[index] = edited[i];
		}
	}
	void updateCurrentPositionFromFirstEdited(bool makeCurrent)
	{
		if(!makeCurrent) return;
		int firstIndex = getPositionIndex(1, 0);
		if(edited[firstIndex] == 1){
			chosen_frame = firstIndex;
			adjX = adjustX[firstIndex];
			adjY = adjustY[firstIndex];
			rot = rotation[firstIndex];
			adjScaleX = scaleX[firstIndex];
			adjScaleY = scaleY[firstIndex];
		}
	}
	int getPositionIndex(int characterSlot, int frameIndex)
	{
		if(characterSlot < 0) characterSlot = 0;
		if(characterSlot >= characterSlotCount) characterSlot = characterSlotCount - 1;
		if(frameIndex < 0) frameIndex = 0;
		if(frameIndex >= FRAMES_PER_CHARACTER) frameIndex = FRAMES_PER_CHARACTER - 1;
		return characterSlot * FRAMES_PER_CHARACTER + frameIndex;
	}
	int coordinateSlotForEditor(int charSlot)
	{
		return charSlot == 0 ? CharsNew::CharAnim::JAZZ : charSlot;
	}
	int getCategoryPositionIndex(int category, int characterSlot, int frameIndex)
	{
		if(category < 0) category = 0;
		if(category >= int(AccessoryCategoryNames.length)) category = int(AccessoryCategoryNames.length) - 1;
		return category * positionArrayLength() + getPositionIndex(characterSlot, frameIndex);
	}
	int accessoryPlayerIndex(int category, int playerID)
	{
		if(category < 0) category = 0;
		if(category >= int(AccessoryCategoryNames.length)) category = int(AccessoryCategoryNames.length) - 1;
		if(playerID < 0) playerID = 0;
		if(playerID >= 32) playerID = 31;
		return category * 32 + playerID;
	}
	int accessoryColorPlayerIndex(int category, int playerID, int colorGroup)
	{
		if(colorGroup < 0) colorGroup = ACCESSORY_COLOR_GROUP_RED;
		if(colorGroup >= ACCESSORY_COLOR_GROUP_COUNT) colorGroup = ACCESSORY_COLOR_GROUP_COUNT - 1;
		return colorGroup * 96 + accessoryPlayerIndex(category, playerID);
	}
	int getAccessoryColorOffset(int category, int playerID, int colorGroup)
	{
		return accessoryColorOffsets[accessoryColorPlayerIndex(category, playerID, colorGroup)];
	}
	bool hasAccessoryColorOffset(int category, int playerID)
	{
		for(int colorGroup = 0; colorGroup < ACCESSORY_COLOR_GROUP_COUNT; colorGroup++)
			if(getAccessoryColorOffset(category, playerID, colorGroup) != 0)
				return true;
		return false;
	}
	void setAccessoryFrame(int category, int playerID, int frame)
	{
		int count = accessoryFrameCount(category);
		if(count <= 0) count = 1;
		int index = accessoryPlayerIndex(category, playerID);
		accessoryFrames[index] = frame % count;
		if(accessoryFrames[index] < 0) accessoryFrames[index] += count;
		noAccessory[index] = false;
		if(category == AccessoryCategory::HATS)
		{
			hats[playerID] = accessoryFrames[index];
			nohat[playerID] = false;
		}
	}
	void setAccessoryColorOffset(int category, int playerID, int colorGroup, int offset)
	{
		accessoryColorOffsets[accessoryColorPlayerIndex(category, playerID, colorGroup)] = offset;
		if(offset != 0)
			ensureAccessoryColorMaskAnimation();
	}
	void setAccessoryColorOffset(int category, int playerID, int offset)
	{
		setAccessoryColorOffset(category, playerID, ACCESSORY_COLOR_GROUP_RED, offset);
	}
	void removeAccessory(int category, int playerID)
	{
		int index = accessoryPlayerIndex(category, playerID);
		noAccessory[index] = true;
		if(category == AccessoryCategory::HATS)
			nohat[playerID] = true;
	}
	bool isAccessoryHidden(int category, int playerID)
	{
		if(category == AccessoryCategory::HATS)
			return nohat[playerID] || noAccessory[accessoryPlayerIndex(category, playerID)];
		return noAccessory[accessoryPlayerIndex(category, playerID)];
	}
	void debugHatFrameColors(int playerID)
	{
		if(playerID < 0 || playerID >= 32) return;
		if(!hatAnimLoaded)
		{
			jjAlert("Hat color debug: XmasHats.j2a is not loaded.", false);
			return;
		}
		int index = accessoryPlayerIndex(AccessoryCategory::HATS, playerID);
		int frame = accessoryFrames[index] % accessoryFrameCount(AccessoryCategory::HATS);
		if(frame < 0) frame += accessoryFrameCount(AccessoryCategory::HATS);
		int firstAnim = jjAnimSets[ANIM::CUSTOM[empty]].firstAnim;
		int firstFrame = jjAnimations[firstAnim + AccessoryCategory::HATS].firstFrame;
		int frameID = firstFrame + frame;
		jjPIXELMAP image(jjAnimFrames[frameID]);
		array<bool> seen(256, false);
		for(int x = image.width - 1; x >= 0; x--)
			for(int y = image.height - 1; y >= 0; y--)
				seen[image[x,y]] = true;

		jjAlert("Hat color debug: player " + playerID + ", hat frame " + frame + (isAccessoryHidden(AccessoryCategory::HATS, playerID) ? " (hidden)" : ""), false);
		string line = "Colors:";
		int count = 0;
		for(int color = 0; color < 256; color++)
		{
			if(!seen[color]) continue;
			string next = (count == 0 ? " " : ", ") + color;
			if(jjGetStringWidth(line + next, STRING::SMALL, STRING::NORMAL) > jjResolutionWidth - 32)
			{
				jjAlert(line, false);
				line = "Colors:";
				next = " " + color;
			}
			line += next;
			count++;
		}
		if(count == 0)
			jjAlert("Colors: none", false);
		else
			jjAlert(line, false);
	}
	bool hasVisibleAccessory(int playerID)
	{
		if(playerID < 0 || playerID >= 32) return false;
		for(int category = 0; category < int(AccessoryCategoryNames.length); category++)
			if(!isAccessoryHidden(category, playerID))
				return true;
		return false;
	}
	int legacyCharacterSlotFromBlock(int block)
	{
		if(block == 0) return 1; // Jazz
		if(block == 1) return 2; // Spaz
		if(block == 2) return 4; // Frog
		if(block == 3) return 3; // Lori
		return block;
	}
	bool usePresetCoordinatesForCategory(int category)
	{
		if(category == AccessoryCategory::HATS) return USE_PRESET_COORDINATES_HAT;
		if(category == AccessoryCategory::SCARFS) return USE_PRESET_COORDINATES_SCARFS;
		if(category == AccessoryCategory::GLASSES) return USE_PRESET_COORDINATES_GLASSES;
		return false;
	}
	bool loadPresetCoordinatesForCategory(int category, bool makeCurrent)
	{
		if(category == AccessoryCategory::HATS) return loadPresetHatCoordinates(makeCurrent);
		if(category == AccessoryCategory::SCARFS) return loadPresetScarfCoordinates(makeCurrent);
		if(category == AccessoryCategory::GLASSES) return loadPresetGlassesCoordinates(makeCurrent);
		return false;
	}
	int baseCharacterSlot(jjPLAYER@ play)
	{
		if(CHARS_ENABLED && CharsNew::characters[play.playerID] > 0){
			return CharsNew::characters[play.playerID];
		}
		if(play.charCurr == CHAR::JAZZ) return 1;
		if(play.charCurr == CHAR::SPAZ) return 2;
		if(play.charCurr == CHAR::FROG) return 4;
		if(play.charCurr == CHAR::BIRD) return 5;
		if(play.charCurr == CHAR::BIRD2) return 6;
		if(play.charCurr == CHAR::LORI) return 3;
		return 0;
	}
	int defaultCharacterSlotForCustom(int charSlot)
	{
		if(charSlot == 1 || charSlot == 11) return 1;
		if(charSlot == 3) return 3;
		if(charSlot == 4) return 4;
		if(charSlot == 5 || charSlot == 6) return charSlot;
		return 2;
	}
	int autoPositionIndex(jjPLAYER@ play, int frameIndex, int animRelIndex)
	{
		int charSlot = baseCharacterSlot(play);
		return autoPositionIndexForSlot(charSlot, frameIndex, animRelIndex);
	}
	int autoPositionIndex(jjPLAYER@ play, int frameIndex, int animRelIndex, int fallbackFrameIndex)
	{
		int charSlot = baseCharacterSlot(play);
		return autoPositionIndexForSlot(charSlot, frameIndex, animRelIndex, fallbackFrameIndex);
	}
	int drawAdjustX = 0;
	int drawAdjustY = 0;
	int drawRotation = 0;
	int drawUsing = 0;
	float drawScaleX = 1;
	float drawScaleY = 1;
	void setDrawPositionFromIndex(int category, int index)
	{
		int categoryIndex = category * positionArrayLength() + index;
		drawAdjustX = categoryAdjustX[categoryIndex];
		drawAdjustY = categoryAdjustY[categoryIndex];
		drawRotation = categoryRotation[categoryIndex];
		drawUsing = categoryUsing[categoryIndex];
		drawScaleX = categoryScaleX[categoryIndex];
		drawScaleY = categoryScaleY[categoryIndex];
	}
	int autoPositionIndexForSlot(int charSlot, int frameIndex, int animRelIndex, int fallbackFrameIndex = -1, int category = -1)
	{
		if(category < 0) category = editCategory;
		int k = getPositionIndex(charSlot, frameIndex);

		if(categoryEdited[category * positionArrayLength() + k] == 1) {
			setDrawPositionFromIndex(category, k);
			return k;
		}

		if(fallbackFrameIndex < 0)
			fallbackFrameIndex = frameIndex;

		int fallback = getPositionIndex(defaultCharacterSlotForCustom(charSlot), fallbackFrameIndex);
		setDrawPositionFromIndex(category, fallback);

		if(CHARS_ENABLED
		&& charSlot >= 0
		&& charSlot < int(CharsNew::chars.length))
		{
			if(animRelIndex < 0)
				animRelIndex = 0;

			if(animRelIndex >= ANIMS_PER_SET)
				animRelIndex = ANIMS_PER_SET - 1;

			int animRel = animRelIndex;

			if(animRelIndex < int(CharsNew::chars[charSlot].animRel.length))
				animRel = CharsNew::chars[charSlot].animRel[animRelIndex];

			if(animRel >= 0 && animRel < FRAMES_PER_CHARACTER)
			{
				fallback = getPositionIndex(
					defaultCharacterSlotForCustom(charSlot),
					fallbackFrameIndex
				);

				int offsetX = 0;
				int offsetY = 0;

				if(animRelIndex < int(CharsNew::chars[charSlot].adjustX.length))
					offsetX = CharsNew::chars[charSlot].adjustX[animRelIndex];

				if(animRelIndex < int(CharsNew::chars[charSlot].adjustY.length))
					offsetY = CharsNew::chars[charSlot].adjustY[animRelIndex];

				int categoryFallback = category * positionArrayLength() + fallback;
				drawAdjustX = categoryAdjustX[categoryFallback] + offsetX;
				drawAdjustY = categoryAdjustY[categoryFallback] + offsetY;
				drawRotation = categoryRotation[categoryFallback];
				drawUsing = categoryUsing[categoryFallback];
				drawScaleX = categoryScaleX[categoryFallback];
				drawScaleY = categoryScaleY[categoryFallback];
				return k;
			}
		}

		return fallback;
	}
	int baseAnimationForSlot(int charSlot)
	{
		int baseSlot = defaultCharacterSlotForCustom(charSlot);
		if(baseSlot == 1) return ANIM::JAZZ;
		if(baseSlot == 3) return ANIM::LORI;
		if(baseSlot == 4) return ANIM::FROG;
		if(baseSlot == 5 || baseSlot == 6) return ANIM::BIRD;
		return ANIM::SPAZ;
	}

	bool isHangLedgeAccessorySlot(int charSlot)
	{
		return charSlot == 0 || charSlot == CharsNew::CharAnim::JAZZ || charSlot == CharsNew::CharAnim::SPAZ;
	}

	int hangLedgeAnimSetForSlot(int charSlot)
	{
		return charSlot == CharsNew::CharAnim::SPAZ ? ANIM::SPAZ : ANIM::JAZZ;
	}

	int ledgesEditorAnimSetForSlot(int charSlot)
	{
		if(charSlot == CharsNew::CharAnim::SPAZ)
		{
			if(ledgesSpazLoaded) return ANIM::CUSTOM[ledgesSpazSet];
			if(ledgesSingleSetMode) return ANIM::CUSTOM[ledgesJazzSet];
			return hangLedgeAnimSetForSlot(charSlot);
		}
		if(ledgesJazzLoaded) return ANIM::CUSTOM[ledgesJazzSet];
		return hangLedgeAnimSetForSlot(charSlot);
	}

	int ledgesEditorAnimIDForSlot(int charSlot)
	{
		if(charSlot == CharsNew::CharAnim::SPAZ)
		{
			if(ledgesSpazLoaded) return 0;
			if(ledgesSingleSetMode) return 1;
			return CharsNew::HangLedgeDrawAnim();
		}
		return ledgesJazzLoaded ? 0 : CharsNew::HangLedgeDrawAnim();
	}

	bool ledgesEditorUsesCustomAnim(int charSlot)
	{
		if(charSlot == CharsNew::CharAnim::SPAZ)
			return ledgesSpazLoaded || ledgesSingleSetMode;
		return ledgesJazzLoaded;
	}

	int hangLedgeEditorFrameIndex(int frameID, bool wrapFrame = false)
	{
		int count = jjAnimations[jjAnimSets[ledgesEditorAnimSetForSlot(selectedCharacterSlot)].firstAnim + ledgesEditorAnimIDForSlot(selectedCharacterSlot)].frameCount;
		if(count <= 0) count = 1;
		int drawFrame = frameID;
		if(drawFrame >= count)
			drawFrame = wrapFrame ? drawFrame % count : count - 1;
		if(drawFrame < 0) drawFrame = 0;
		int maxFrame = FRAMES_PER_CHARACTER - HANG_LEDGE_FRAME_INDEX_START - 1;
		if(drawFrame > maxFrame) drawFrame = maxFrame;
		return HANG_LEDGE_FRAME_INDEX_START + drawFrame;
	}

	int animationForPlayer(jjPLAYER@ play)
	{
		if(CHARS_ENABLED && CharsNew::forceFrogVisual[play.playerID]) return ANIM::FROG;
		if(play.charCurr == CHAR::JAZZ) return ANIM::JAZZ;
		if(play.charCurr == CHAR::SPAZ) return ANIM::SPAZ;
		if(play.charCurr == CHAR::FROG) return ANIM::FROG;
		if(play.charCurr == CHAR::LORI) return ANIM::LORI;
		if(play.charCurr == CHAR::BIRD || play.charCurr == CHAR::BIRD2) return ANIM::BIRD;
		return -1;
	}

	int headSourceCharacterSlot(int charSlot)
	{
		if(charSlot == CharsNew::CharAnim::HEAD_MARIO) return CharsNew::CharAnim::MARIO;
		if(charSlot == CharsNew::CharAnim::HEAD_SONIC || charSlot == CharsNew::CharAnim::HEAD_EGGMAN) return CharsNew::CharAnim::SONIC;
		if(charSlot == CharsNew::CharAnim::HEAD_TAILS) return CharsNew::CharAnim::TAILS;
		if(charSlot == CharsNew::CharAnim::HEAD_DEVAN) return CharsNew::CharAnim::DEVAN;
		if(charSlot == CharsNew::CharAnim::HEAD_YOSHI) return CharsNew::CharAnim::YOSHI;
		if(charSlot == CharsNew::CharAnim::HEAD_LORI && !jjIsTSF) return CharsNew::CharAnim::LORI1;
		return charSlot;
	}

	int sourceSetForFrameSlot(int charSlot)
	{
		int sourceCharSlot = headSourceCharacterSlot(charSlot);
		if(sourceCharSlot >= 0 && sourceCharSlot < int(CharsNew::chars.length))
			return CharsNew::chars[sourceCharSlot].CharactersMinSourceSet;
		return -1;
	}

	int coordinateSourceSetForFrameSlot(int charSlot)
	{
		int sourceCharSlot = headSourceCharacterSlot(charSlot);
		int sourceSet = sourceSetForFrameSlot(charSlot);
		if(sourceCharSlot == CharsNew::CharAnim::YOSHI && sourceSet == CharactersMinAnim::SET_BLUEYOSHI)
			return CharactersMinAnim::SET_YOSHI;
		return sourceSet;
	}

	bool isHeadSlot(int charSlot)
	{
		return CharsNew::isPlayerHead(charSlot) || CharsNew::isCustomHead(charSlot);
	}

	int editorAnimationForSlot(int charSlot)
	{
		if(charSlot == CharsNew::CharAnim::WITCH) return ANIM::WITCH;
		if(charSlot == CharsNew::CharAnim::EVA) return ANIM::EVA;
		if(isHeadSlot(charSlot)
		&& charSlot >= 0
		&& charSlot < int(CharsNew::chars.length))
			return CharsNew::chars[charSlot].HeadAnimSet;
		if(CHARS_ENABLED
		&& charSlot >= 0
		&& charSlot < int(CharsNew::chars.length)
		&& CharsNew::chars[charSlot].Set != -1)
			return ANIM::CUSTOM[CharsNew::chars[charSlot].Set];
		return baseAnimationForSlot(charSlot);
	}

	int editorAnimationForAnimID(int charSlot, int animID)
	{
		if(charSlot == CharsNew::CharAnim::EVA && (animID == 5 || animID == 6))
			return ANIM::FLAG;
		return editorAnimationForSlot(charSlot);
	}

	bool isSpecialAccessorySlot(int charSlot)
	{
		return charSlot == CharsNew::CharAnim::WITCH
			|| charSlot == CharsNew::CharAnim::EVA
			|| isHeadSlot(charSlot);
	}

	int headOffsetXForDirection(int charSlot, int direction)
	{
		if(charSlot == CharsNew::CharAnim::HEAD_JAZZ || charSlot == CharsNew::CharAnim::HEAD_SPAZ || charSlot == CharsNew::CharAnim::HEAD_LORI)
			return direction == 1 ? 16 : -24;
		if(charSlot == CharsNew::CharAnim::HEAD_FROG)
			return direction == 1 ? 14 : -18;
		if(charSlot == CharsNew::CharAnim::HEAD_BIRD || charSlot == CharsNew::CharAnim::HEAD_BIRD2)
			return direction == 1 ? 8 : -16;
		if(charSlot == CharsNew::CharAnim::HEAD_MARIO || charSlot == CharsNew::CharAnim::HEAD_SONIC)
			return direction == 1 ? 14 : -18;
		if(charSlot == CharsNew::CharAnim::HEAD_TAILS || charSlot == CharsNew::CharAnim::HEAD_DEVAN)
			return direction == 1 ? 15 : -20;
		if(charSlot == CharsNew::CharAnim::HEAD_YOSHI)
			return direction == 1 ? 12 : -12;
		return 0;
	}

	float headScaleForSlot(int charSlot)
	{
		if(charSlot == CharsNew::CharAnim::HEAD_TAILS || charSlot == CharsNew::CharAnim::HEAD_YOSHI)
			return 1.5;
		return 1;
	}

	int playerAnimRelIndex(jjPLAYER@ play)
	{
		return CharsNew::PlayerRabbitAnimRel(play);
	}

	// The real Spaz bird animations are stored separately from the ordinary
	// rabbit list. 78 and 79 are real coordinate targets; do not reserve them
	// for the ledge-editor preview.
	const int SPAZ_BIRD_ANIM_FIRST = 78;
	const int SPAZ_BIRD_ANIM_LAST = 79;
	array<int> rabbit123JazzPackedOffset(128, -1);
	array<int> rabbit123JazzFrameCount(128, 0);
	array<int> rabbit123SpazPackedOffset(128, -1);
	array<int> rabbit123SpazFrameCount(128, 0);

	bool isSpazBirdEditorAnim(int charSlot, int animID)
	{
		return charSlot == CharsNew::CharAnim::SPAZ
			&& animID >= SPAZ_BIRD_ANIM_FIRST
			&& animID <= SPAZ_BIRD_ANIM_LAST;
	}

	int spazBirdLocalAnimID(int animID)
	{
		return animID - SPAZ_BIRD_ANIM_FIRST;
	}

	int rabbit123NativeAnimFor124(int anim124)
	{
		if(anim124 >= 0 && anim124 < int(CharsNew::animIndex.length))
			return CharsNew::animIndex[anim124];

		// These are the two native 1.23 replacement animations for Spaz's bird.
		if(anim124 == 78) return 104;
		if(anim124 == 79) return 105;
		return -1;
	}

	void buildRabbit123PackedFrameCacheForSet(int animSet, array<int>@ offsets, array<int>@ counts)
	{
		for(uint i = 0; i < offsets.length; i++)
		{
			offsets[i] = -1;
			counts[i] = 0;
		}
		if(jjIsTSF || (animSet != ANIM::JAZZ && animSet != ANIM::SPAZ)) return;

		int firstAnim = jjAnimSets[animSet].firstAnim;
		int packedFrame = 0;
		for(int anim124 = 0; anim124 < int(offsets.length); anim124++)
		{
			int nativeAnim = rabbit123NativeAnimFor124(anim124);
			if(nativeAnim < 0) continue;
			int count = jjAnimations[firstAnim + nativeAnim].frameCount;
			if(count <= 0) continue;
			offsets[anim124] = packedFrame;
			counts[anim124] = count;
			packedFrame += count;
		}
	}

	void buildRabbit123PackedFrameCache()
	{
		buildRabbit123PackedFrameCacheForSet(ANIM::JAZZ, @rabbit123JazzPackedOffset, @rabbit123JazzFrameCount);
		buildRabbit123PackedFrameCacheForSet(ANIM::SPAZ, @rabbit123SpazPackedOffset, @rabbit123SpazFrameCount);
	}

	int rabbit123PackedFrameFor124(int animSet, int anim124, int frameInAnimation)
	{
		if(animSet != ANIM::JAZZ && animSet != ANIM::SPAZ)
			return -1;

		int nativeAnim = rabbit123NativeAnimFor124(anim124);
		if(nativeAnim < 0)
			return -1;

		if(!jjIsTSF && anim124 >= 0 && anim124 < 128)
		{
			int offset = animSet == ANIM::JAZZ ? rabbit123JazzPackedOffset[anim124] : rabbit123SpazPackedOffset[anim124];
			int count = animSet == ANIM::JAZZ ? rabbit123JazzFrameCount[anim124] : rabbit123SpazFrameCount[anim124];
			if(count <= 0 || offset < 0) return -1;
			if(frameInAnimation < 0) frameInAnimation = 0;
			if(frameInAnimation >= count) frameInAnimation = count - 1;
			return offset + frameInAnimation;
		}

		int firstAnim = jjAnimSets[animSet].firstAnim;
		int packedFrame = 0;

		for(int i = 0; i < anim124; i++)
		{
			int nativeBefore = rabbit123NativeAnimFor124(i);
			if(nativeBefore < 0)
				return -1;

			int count = jjAnimations[firstAnim + nativeBefore].frameCount;
			if(count > 0)
				packedFrame += count;
		}

		int count = jjAnimations[firstAnim + nativeAnim].frameCount;
		if(count <= 0)
			return -1;

		if(frameInAnimation < 0) frameInAnimation = 0;
		if(frameInAnimation >= count) frameInAnimation = count - 1;
		return packedFrame + frameInAnimation;
	}

	int spazBirdEditorFrameIndex(int animID, int frameID, bool wrapFrame = false)
	{
		if(!isSpazBirdEditorAnim(CharsNew::CharAnim::SPAZ, animID))
			return -1;

		int localAnim = spazBirdLocalAnimID(animID);
		int specialSet = ANIM::SPAZ2;
		int count = jjAnimations[jjAnimSets[specialSet].firstAnim + localAnim].frameCount;
		if(count <= 0)
			return -1;

		int drawFrame = frameID;
		if(drawFrame < 0) drawFrame = 0;
		else if(drawFrame >= count)
			drawFrame = wrapFrame ? drawFrame % count : count - 1;

		// 1.23 coordinate files use the canonical 1.24 packed rows.
		if(!jjIsTSF)
			return rabbit123PackedFrameFor124(ANIM::SPAZ, animID, drawFrame);

		// In TSF the separate SPAZ2 frames are physically packed after the
		// normal SPAZ frames, which is the same coordinate numbering used in
		// the hat data.
		return jjAnimations[jjAnimSets[specialSet].firstAnim + localAnim].firstFrame
			+ drawFrame
			- jjAnimations[jjAnimSets[ANIM::SPAZ].firstAnim].firstFrame;
	}

	// Convert a native 1.23 Jazz/Spaz frame to the packed 1.24 coordinate row.
	// This includes native 104/105, the two separate Spaz bird animations.
	int rabbit123PackedFrameIndex(int animSet, int nativeAnim, jjPLAYER@ play)
	{
		if(jjIsTSF || play is null)
			return -1;

		int anim124 = CharsNew::revertIndex(nativeAnim);
		if(anim124 < 0 || rabbit123NativeAnimFor124(anim124) != nativeAnim)
			return -1;

		int currentCount = jjAnimations[jjAnimSets[animSet].firstAnim + nativeAnim].frameCount;
		if(currentCount <= 0)
			return -1;

		int frameInAnimation = play.frameID;
		if(frameInAnimation < 0 || frameInAnimation >= currentCount)
			frameInAnimation = play.curFrame
				- jjAnimations[jjAnimSets[animSet].firstAnim + nativeAnim].firstFrame;

		return rabbit123PackedFrameFor124(animSet, anim124, frameInAnimation);
	}
	int animationForSlot(int charSlot, jjPLAYER@ play)
	{
		if(CHARS_ENABLED
		&& charSlot >= 0
		&& uint(charSlot) < CharsNew::chars.length
		&& CharsNew::chars[charSlot].Set != -1)
			return ANIM::CUSTOM[CharsNew::chars[charSlot].Set];
		return animationForPlayer(play);
	}
	int frameIndexForAnimFrame(int animSet, int animID, int frameID, int charSlot = -1, bool wrapFrame = false)
	{
		if(animSet == -1 || animID < 0)
			return -1;
		int sourceCharSlot = charSlot >= 0 ? charSlot : selectedCharacterSlot;
		if(isSpazBirdEditorAnim(sourceCharSlot, animID))
			return spazBirdEditorFrameIndex(animID, frameID, wrapFrame);
		if(animID == CharsNew::HANG_LEDGE_EDITOR_ANIM && isHangLedgeAccessorySlot(sourceCharSlot))
			return hangLedgeEditorFrameIndex(frameID, wrapFrame);
		if(sourceCharSlot == CharsNew::CharAnim::DEVAN)
		{
			if(animID < 0 || animID >= int(CharsNew::DevanAnim::WARP_OUT_GUN) + 1)
				return -1;
			int firstAnim = jjAnimSets[ANIM::DEVILDEVAN].firstAnim;
			int frameCount = jjAnimations[firstAnim + animID].frameCount;
			int drawFrame = frameID;
			if(frameCount <= 0)
				drawFrame = 0;
			else if(drawFrame >= frameCount)
				drawFrame = wrapFrame ? drawFrame % frameCount : frameCount - 1;
			return jjAnimations[firstAnim + animID].firstFrame + drawFrame
				- jjAnimations[firstAnim].firstFrame;
		}
		int sourceSet = coordinateSourceSetForFrameSlot(sourceCharSlot);
		int mappedAnim = 0;
		int mappedFrame = 0;
		if(!CharsNew::RemapCharactersMinFrame(animSet, sourceSet, animID, frameID, mappedAnim, mappedFrame))
			return -1;

		int firstAnim = jjAnimSets[animSet].firstAnim;
		int frameCount = jjAnimations[firstAnim + mappedAnim].frameCount;
		int drawFrame = mappedFrame;

		if(frameCount <= 0)
			drawFrame = 0;
		else if(drawFrame >= frameCount)
			drawFrame = wrapFrame ? drawFrame % frameCount : frameCount - 1;

		return jjAnimations[firstAnim + mappedAnim].firstFrame + drawFrame
			- jjAnimations[firstAnim].firstFrame;
	}
	
	int add = 0;
	void readFileForCategory(int category, bool makeCurrent)
	{
		bool loadedPreset = usePresetCoordinatesForCategory(category) && loadPresetCoordinatesForCategory(category, makeCurrent);
		if(!loadedPreset)
		{
			resetCategoryPositionArrays(category);
			if(makeCurrent) resetPositionArrays();
		}
		add = 0;
		string filename = category >= 0 && category < int(AccessoryCoordinateFilenames.length)
			? AccessoryCoordinateFilenames[category]
			: AccessoryCoordinateFilenames[AccessoryCategory::HATS];
		jjSTREAM load(filename);
		while(!load.isEmpty()){
			string str = "";
			load.getLine(str);
			array<string> string_parts = str.split(" ");
			if(string_parts.length < 5) continue;
			int frameIndex = parseInt(string_parts[0]);
			int slot = legacyCharacterSlotFromBlock(add);
			bool hasSavedSlot = false;
			if(string_parts.length > 5) {
				int savedSlot = parseInt(string_parts[5]);
				if(savedSlot > 0) {
					slot = savedSlot;
					hasSavedSlot = true;
				}
			}
			int index = getPositionIndex(slot, frameIndex);
			int categoryIndex = getCategoryPositionIndex(category, slot, frameIndex);
			categoryAdjustX[categoryIndex] = parseInt(string_parts[1]);
			categoryAdjustY[categoryIndex] = parseInt(string_parts[2]);
			categoryRotation[categoryIndex] = parseInt(string_parts[3]);
			categoryUsing[categoryIndex] = parseInt(string_parts[4]);
			categoryScaleX[categoryIndex] = 1;
			categoryScaleY[categoryIndex] = 1;
			if(string_parts.length > 7 && string_parts[6] != "" && string_parts[7] != ""){
				categoryScaleX[categoryIndex] = parseFloat(string_parts[6]);
				categoryScaleY[categoryIndex] = parseFloat(string_parts[7]);
			}
			categoryEdited[categoryIndex] = 1;
			if(frameIndex == FRAMES_PER_CHARACTER - 1 && !hasSavedSlot){
				add+=1;
				//jjAlert("added "+add, false);
			}
		}
		if(makeCurrent) copyCategoryToCurrent(category);
		updateCurrentPositionFromFirstEdited(makeCurrent);
	}
	void readFile(){
		readFileForCategory(editCategory, true);
		currentCategoryLoaded = true;
	}
	void readAllCategoryFiles()
	{
		for(int category = 0; category < int(AccessoryCategoryNames.length); category++)
			readFileForCategory(category, category == editCategory);
		currentCategoryLoaded = true;
	}
	bool read = false;
	array<int> yoshiDebugUntil(32, 0);
	array<bool> yoshiDebugJumpWasDown(32, false);
	array<string> yoshiDebugLast(32, "");

	void updateYoshiDebugTrigger(jjPLAYER@ play, int charSlot)
	{
		if(!DEBUG_YOSHI_ON || play.playerID < 0 || play.playerID >= 32) return;
		bool isYoshi = charSlot == CharsNew::CharAnim::YOSHI;
		bool jumpDown = isYoshi && play.keyJump;
		if(jumpDown && !yoshiDebugJumpWasDown[play.playerID]){
			yoshiDebugUntil[play.playerID] = jjGameTicks + 140;
			yoshiDebugLast[play.playerID] = "";
		}
		yoshiDebugJumpWasDown[play.playerID] = jumpDown;
	}

	void alertYoshiDebug(jjPLAYER@ play, int charSlot, int animSet, int animID, int frameID, int frameIndex, int positionIndex)
	{
		if(!DEBUG_YOSHI_ON || charSlot != CharsNew::CharAnim::YOSHI) return;
		if(play.playerID < 0 || play.playerID >= 32) return;
		if(jjGameTicks > yoshiDebugUntil[play.playerID]) return;
		string key = animSet + ":" + animID + ":" + frameID + ":" + frameIndex + ":" + drawAdjustX + ":" + drawAdjustY + ":" + drawRotation + ":" + drawScaleX + ":" + drawScaleY;
		if(key == yoshiDebugLast[play.playerID]) return;
		yoshiDebugLast[play.playerID] = key;
		jjAlert(
			"Yoshi set " + animSet
			+ " anim " + animID
			+ " frame " + frameID
			+ " index " + frameIndex
			+ " pos " + positionIndex
			+ " hat x " + drawAdjustX
			+ " y " + drawAdjustY
			+ " rot " + drawRotation
			+ " sx " + drawScaleX
			+ " sy " + drawScaleY,
			false
		);
	}

	int effectiveAccessoryMappedAnim(jjPLAYER@ play, int charSlot, int animRelIndex, int mappedAnim)
	{
		if(charSlot == CharsNew::CharAnim::SONIC || charSlot == CharsNew::CharAnim::MARIO)
		{
			int rawAnim = play.curAnim - jjAnimSets[play.setID].firstAnim;
			if(play.setID == ANIM::SPAZ && rawAnim > 77)
				return jjIsTSF ? 33 : 30;
			else if(play.setID == ANIM::SPAZ2 && rawAnim <= 3)
				return mappedAnim + (jjIsTSF ? 78 : 104);
		}
		return mappedAnim;
	}

	int effectiveAccessoryFrame(jjPLAYER@ play, int charSlot, int animRelIndex, int mappedAnim)
	{
		if(charSlot == CharsNew::CharAnim::YOSHI)
		{
			if(mappedAnim == 13 || mappedAnim == 48) return 0;
			if(animRelIndex == 5 || animRelIndex == 44) return 1;
			if(animRelIndex == 14) return jjGameTicks / 10;
			return play.frameID;
		}
		if(charSlot == CharsNew::CharAnim::JJ1)
		{
			if(mappedAnim == 11) return jjGameTicks / 10;
			if(animRelIndex < 78 && animRelIndex >= 0)
				return animRelIndex == 20 ? jjGameTicks / 10 : play.frameID;
			return play.frameID;
		}
		if(charSlot == CharsNew::CharAnim::TAILS)
		{
			if(mappedAnim == 16) return jjGameTicks / 10;
			if(mappedAnim == 12) return 0;
			if(mappedAnim == 6) return (jjGameTicks / 10) % 8;
			return jjGameTicks / 10;
		}
		return play.frameID;
	}

	bool specialAccessoryFrame(jjPLAYER@ play, int charSlot, int &out animSet, int &out animID, int &out frameID, int &out frameIndex)
	{
		if(isHangLedgeAccessorySlot(charSlot) && CharsNew::IsHangLedgeActive(play.playerID))
		{
			animSet = baseAnimationForSlot(charSlot);
			animID = CharsNew::HANG_LEDGE_EDITOR_ANIM;
			frameID = CharsNew::HangLedgeFrame(play.playerID);
			frameIndex = frameIndexForAnimFrame(animSet, animID, frameID, charSlot, true);
			return frameIndex >= 0;
		}

		if(charSlot == CharsNew::CharAnim::WITCH)
		{
			animSet = ANIM::WITCH;
			animID = play.health > 0 ? (play.keyFire ? 0 : 2) : 1;
			frameID = play.health > 0 ? jjGameTicks / 10 : jjGameTicks / 3;
			frameIndex = frameIndexForAnimFrame(animSet, animID, frameID, charSlot, true);
			return frameIndex >= 0;
		}

		if(charSlot == CharsNew::CharAnim::EVA)
		{
			int ind = CharsNew::PlayerRabbitAnimRelForSet(play, ANIM::SPAZ);
			int currentEvaFrame = CharsNew::evaFrame[play.playerID];
			if(currentEvaFrame == -1 && (play.specialMove > 0 || (62 <= ind && ind <= 64)))
				currentEvaFrame = 0;

			if(currentEvaFrame == -1)
			{
				if(48 <= ind && ind <= 50){
					animSet = ANIM::FLAG;
					animID = 5;
					frameID = jjGameTicks / 10;
				}else if(ind == 11 || (51 <= ind && ind <= 53) || (37 <= ind && ind <= 39) || ind == 15 || ind == 44){
					animSet = ANIM::FLAG;
					animID = 6;
					frameID = jjGameTicks / 10;
				}else{
					animSet = ANIM::EVA;
					animID = 0;
					frameID = jjGameTicks / 20;
				}
			}else{
				animSet = ANIM::EVA;
				animID = currentEvaFrame > 13 ? 3 : 2;
				frameID = currentEvaFrame > 13 ? (currentEvaFrame - 14) : currentEvaFrame;
			}

			frameIndex = frameIndexForAnimFrame(animSet, animID, frameID, charSlot, true);
			return frameIndex >= 0;
		}

		if(isHeadSlot(charSlot)
		&& charSlot >= 0
		&& charSlot < int(CharsNew::chars.length)
		&& CharsNew::chars[charSlot].LivesAnim >= 0)
		{
			animSet = CharsNew::chars[charSlot].HeadAnimSet;
			animID = CharsNew::chars[charSlot].LivesAnim;
			frameID = jjGameTicks / 10;
			frameIndex = frameIndexForAnimFrame(animSet, animID, frameID, charSlot, true);
			return frameIndex >= 0;
		}

		return false;
	}

	void onMain(){
		if(!currentCategoryLoaded){
			//jjAlert("read");
			readAllCategoryFiles();
			read = true;
		}
		for(uint i=0; i<32; i++){
			if(jjPlayers[i].isActive){
				jjPLAYER@ play = jjPlayers[i];
				if(play.isInGame){
					if(!play.isLocal && !CharsNew::isPlayerVisible(play.playerID, 128))
						continue;
					if(!hasVisibleAccessory(play.playerID))
						continue;

					// Nyan Rush owns the complete player visual while active.
					// Skip every runtime accessory category (hat, scarf, glasses, etc.)
					// without changing the player's saved accessory selections.
					if(CharsNyanRush::ShouldSuppressVisuals(play.playerID))
						continue;

					if(MONSTERS_ON && CharsMonsters::ShouldSuppressHats(play.playerID))
						continue;
					//Draw Hat
					{//Draw hats for normal, custom, frog, and bird morphs.
						int charSlot = baseCharacterSlot(play);
						updateYoshiDebugTrigger(play, charSlot);
						int animSet = animationForPlayer(play);
						if(animSet == -1)
							continue;
						int animRelIndex = play.curAnim - jjAnimSets[animSet].firstAnim;
						int frameIndex = play.curFrame - jjAnimations[jjAnimSets[animSet].firstAnim].firstFrame;
						if(CHARS_ENABLED && CharsNew::forceFrogVisual[play.playerID])
						{
							animRelIndex = CharsNew::ForcedFrogAnimID(play);
							frameIndex = CharsNew::ForcedFrogFrameIndex(play);
						}

						// Ordinary 1.23 Jazz/Spaz hats use rows packed in 1.24 order.
						// Convert both the native animation and the packed-frame position.
						// Hanging uses its dedicated branch below and replaces these values.
						if(!jjIsTSF
						&& (charSlot == CharsNew::CharAnim::JAZZ
							|| charSlot == CharsNew::CharAnim::SPAZ))
						{
							int convertedFrameIndex = rabbit123PackedFrameIndex(
								animSet,
								animRelIndex,
								play
							);

							if(convertedFrameIndex >= 0)
							{
								animRelIndex = CharsNew::revertIndex(animRelIndex);
								frameIndex = convertedFrameIndex;
							}
						}

						int fallbackFrameIndex = frameIndex;
						int debugAnimSet = animSet;
						int debugAnimID = animRelIndex;
						int debugFrameID = play.frameID;
						int accessoryBaseOffsetX = 0;
						float accessoryBaseOffsetY = 0;
						float accessoryBaseScale = 1;
						if(CHARS_ENABLED
						&& play.fly == FLIGHT::AIRBOARD
						&& (charSlot == CharsNew::CharAnim::JAZZ
							|| charSlot == CharsNew::CharAnim::SPAZ
							|| charSlot == CharsNew::CharAnim::LORI
							|| charSlot == CharsNew::CharAnim::LORI1))
						{
							accessoryBaseOffsetY = CharsNew::AirboardWobbleYOffset(play);
						}

						bool hangingOnLedge = CHARS_ENABLED
							&& isHangLedgeAccessorySlot(charSlot)
							&& CharsNew::IsHangLedgeActive(play.playerID)
							&& !(MONSTERS_ON && CharsMonsters::IsActive(play.playerID));

						if(hangingOnLedge)
						{
							int specialAnimSet = animSet;
							int specialAnimID = animRelIndex;
							int specialFrameID = play.frameID;
							int specialFrameIndex = frameIndex;
							if(specialAccessoryFrame(play, charSlot, specialAnimSet, specialAnimID, specialFrameID, specialFrameIndex))
							{
								animRelIndex = specialAnimID;
								frameIndex = specialFrameIndex;
								fallbackFrameIndex = specialFrameIndex;
								debugAnimSet = specialAnimSet;
								debugAnimID = specialAnimID;
								debugFrameID = specialFrameID;
							}
							accessoryBaseOffsetY = charSlot == CharsNew::CharAnim::SPAZ
								? HANG_LEDGE_RUNTIME_ACCESSORY_Y_OFFSET_SPAZ
								: HANG_LEDGE_RUNTIME_ACCESSORY_Y_OFFSET_JAZZ;
						}
						else if(CHARS_ENABLED && isSpecialAccessorySlot(charSlot))
						{
							int specialAnimSet = animSet;
							int specialAnimID = animRelIndex;
							int specialFrameID = play.frameID;
							int specialFrameIndex = frameIndex;
							if(specialAccessoryFrame(play, charSlot, specialAnimSet, specialAnimID, specialFrameID, specialFrameIndex))
							{
								animRelIndex = specialAnimID;
								frameIndex = specialFrameIndex;
								fallbackFrameIndex = specialFrameIndex;
								debugAnimSet = specialAnimSet;
								debugAnimID = specialAnimID;
								debugFrameID = specialFrameID;
							}
							if(charSlot == CharsNew::CharAnim::EVA)
								accessoryBaseOffsetY = -4;
							else if(isHeadSlot(charSlot)
							&& charSlot >= 0
							&& charSlot < int(CharsNew::chars.length))
							{
								accessoryBaseOffsetX = -headOffsetXForDirection(charSlot, play.direction);
								accessoryBaseOffsetY = CharsNew::chars[charSlot].HeadOffsetY;
								accessoryBaseScale = headScaleForSlot(charSlot);
							}
						}
						else if(CHARS_ENABLED && charSlot == CharsNew::CharAnim::DEVAN)
						{
							animRelIndex = int(CharsNew::var[play.playerID].anim);
							int devanFrame = int(CharsNew::var[play.playerID].frame);
							frameIndex = frameIndexForAnimFrame(ANIM::DEVILDEVAN, animRelIndex, devanFrame, charSlot);
							fallbackFrameIndex = frameIndex;
							debugAnimSet = ANIM::DEVILDEVAN;
							debugAnimID = animRelIndex;
							debugFrameID = devanFrame;
						}
						else if(charSlot > 4 && CHARS_ENABLED
						&& charSlot >= 0
						&& uint(charSlot) < CharsNew::chars.length
						&& CharsNew::chars[charSlot].Set != -1)
						{
							animRelIndex = playerAnimRelIndex(play);
							if(animRelIndex < 0 || animRelIndex >= ANIMS_PER_SET)
								continue;
							int mappedAnim = getMappedEditAnimID(charSlot, animRelIndex);
							mappedAnim = effectiveAccessoryMappedAnim(play, charSlot, animRelIndex, mappedAnim);
							int effectiveFrame = effectiveAccessoryFrame(play, charSlot, animRelIndex, mappedAnim);
							frameIndex = frameIndexForAnimFrame(ANIM::CUSTOM[CharsNew::chars[charSlot].Set], mappedAnim, effectiveFrame, charSlot);
							fallbackFrameIndex = frameIndexForAnimFrame(animSet, play.curAnim - jjAnimSets[animSet].firstAnim, play.frameID, defaultCharacterSlotForCustom(charSlot));
							debugAnimSet = ANIM::CUSTOM[CharsNew::chars[charSlot].Set];
							debugAnimID = mappedAnim;
							debugFrameID = effectiveFrame;
						}
						if(CHARS_ENABLED && charSlot == CharsNew::CharAnim::DEVAN)
						{
							accessoryBaseOffsetX += play.direction > 0 ? DEVAN_RUNTIME_ACCESSORY_X_OFFSET : -DEVAN_RUNTIME_ACCESSORY_X_OFFSET;
							accessoryBaseOffsetY += DEVAN_RUNTIME_ACCESSORY_Y_OFFSET;
						}
						
						//if(jjGameTicks%70 == 0 && jjIsServer) jjAlert("k="+k, false);
						if(frameIndex >= 0 && frameIndex < FRAMES_PER_CHARACTER){
							int dir = play.direction;
							int grav = play.antiGrav ? -1 : 1;
							if(CharsNew::charsNewHideActive[play.playerID])
								continue;
							float charScale = CHARS_ENABLED ? CharsNew::PlayerSizeScale(play.playerID) : 1;
							float scaledPlayerYOffset = (1 - charScale) * 23 * grav;
							for(int category = 0; category < int(AccessoryCategoryNames.length); category++){
								if(isAccessoryHidden(category, play.playerID))
									continue;
								int k = autoPositionIndexForSlot(charSlot, frameIndex, animRelIndex, fallbackFrameIndex, category);
								if(category == AccessoryCategory::HATS)
									alertYoshiDebug(play, charSlot, debugAnimSet, debugAnimID, debugFrameID, frameIndex, k);

								// The ledge body uses these CharsNew offsets after its normal
								// tiny/giant feet anchor. Apply the exact same extra correction
								// to hats so changing the core value also moves the hat.
								float hangLedgeHatSizeYOffset = 0;
								if(category == AccessoryCategory::HATS && hangingOnLedge)
								{
									if(charScale < 1)
										hangLedgeHatSizeYOffset = CharsNew::HANG_LEDGE_TINY_Y_OFFSET;
									else if(charScale > 1)
										hangLedgeHatSizeYOffset = CharsNew::HANG_LEDGE_GIANT_Y_OFFSET;
								}

								int accessoryIndex = accessoryPlayerIndex(category, play.playerID);
								int accessoryFrame = accessoryFrames[accessoryIndex] % accessoryFrameCount(category);

								// This is a local-X adjustment, so multiplying by dir makes it
								// mirror correctly when Yoshi faces left.
								float normalYoshiRunningHatOffsetX = 0;
								if(category == AccessoryCategory::HATS
								&& charSlot == CharsNew::CharAnim::YOSHI
								&& animRelIndex == 56
								&& sourceSetForFrameSlot(charSlot) != CharactersMinAnim::SET_BLUEYOSHI)
								{
									normalYoshiRunningHatOffsetX =
										NORMAL_YOSHI_RUNNING_56_HAT_X_OFFSET * dir * charScale;
								}

								float accessoryDrawX = play.xPos + accessoryBaseOffsetX + normalYoshiRunningHatOffsetX + drawAdjustX*dir*charScale + (dir == -1 ? charScale : 0);
								float accessoryDrawY = play.yPos + accessoryBaseOffsetY*grav + scaledPlayerYOffset + hangLedgeHatSizeYOffset + drawAdjustY*grav*charScale + (grav == 1 ? 0 : charScale);
								int accessoryDrawRotation = drawRotation*dir*grav;
								float accessoryDrawScaleX = accessoryBaseMultiplier*accessoryBaseScale*charScale*play.direction*drawScaleX;
								float accessoryDrawScaleY = accessoryBaseMultiplier*accessoryBaseScale*charScale*grav*drawScaleY;

								jjDrawRotatedSprite(
									accessoryDrawX,
									accessoryDrawY,
									ANIM::CUSTOM[empty], 
									category,
									accessoryFrame,
									accessoryDrawRotation, 
									accessoryDrawScaleX,
									accessoryDrawScaleY,
									SPRITE::NORMAL,
									0, 4, 4, -1);
								if(hasAccessoryColorOffset(category, play.playerID))
								{
									ensureAccessoryColorMaskAnimation();
								}
								if(accessoryColorMaskAnimLoaded && hasAccessoryColorOffset(category, play.playerID))
								{
									for(int colorGroup = 0; colorGroup < ACCESSORY_COLOR_GROUP_COUNT; colorGroup++){
										int accessoryColorOffset = getAccessoryColorOffset(category, play.playerID, colorGroup);
										if(accessoryColorOffset == 0) continue;
										int maskFrame = accessoryColorMaskFrame(colorGroup, category, accessoryFrame);
										if(maskFrame >= 0)
											jjDrawRotatedSprite(
												accessoryDrawX,
												accessoryDrawY,
												ANIM::CUSTOM[accessoryColorMaskSet],
												0,
												maskFrame,
												accessoryDrawRotation,
												accessoryDrawScaleX,
												accessoryDrawScaleY,
												SPRITE::PALSHIFT,
												accessoryColorOffset, 4, 4, -1);
									}
								}
							}
						}
					}
				}
			}
		}
	}
	void saveEdit(){
		clampCurrentFrame();
		int frameIndex = frameIndexForAnimFrame(editorAnimationForAnimID(selectedCharacterSlot, anim_id), anim_id, frame_id, selectedCharacterSlot);
		if(frameIndex < 0) return;
		chosen_frame = getPositionIndex(coordinateSlotForEditor(selectedCharacterSlot), frameIndex);
		adjustX[chosen_frame] = adjX;
		adjustY[chosen_frame] = adjY;
		rotation[chosen_frame] = rot;
		using[chosen_frame] = use;
		scaleX[chosen_frame] = adjScaleX;
		scaleY[chosen_frame] = adjScaleY;
		edited[chosen_frame] = 1;
		//jjAlert(chosen_frame+" saved");
	}
	void saveFile(){
		clampCurrentFrame();
		int frameIndex = frameIndexForAnimFrame(editorAnimationForAnimID(selectedCharacterSlot, anim_id), anim_id, frame_id, selectedCharacterSlot);
		if(frameIndex < 0) return;
		chosen_frame = getPositionIndex(coordinateSlotForEditor(selectedCharacterSlot), frameIndex);
		copyCurrentToCategory(editCategory);
		string filename = currentCoordinateFilename();
		int existingRows = countCoordinateRows(filename);
		int savedRows = 0;
		jjSTREAM save;
		for(int slot = 1; slot < characterSlotCount; slot++){
			for(int frame = 0; frame < FRAMES_PER_CHARACTER; frame++){
				int i = getPositionIndex(slot, frame);
				int categoryIndex = editCategory * positionArrayLength() + i;
				if(categoryEdited[categoryIndex] == 1){
					string str = frame+" "+categoryAdjustX[categoryIndex]+" "+categoryAdjustY[categoryIndex]+" "+categoryRotation[categoryIndex]+" "+categoryUsing[categoryIndex]+" "+slot+" "+categoryScaleX[categoryIndex]+" "+categoryScaleY[categoryIndex]+" \n";
					save.write(str);
					savedRows++;
				}
			}
		}
		if(existingRows > 20 && savedRows < existingRows / 2){
			jjAlert("Refusing to shrink " + filename + " from " + existingRows + " rows to " + savedRows + ". Save blocked.", false);
			return;
		}
		save.save(filename);
	}

	int countCoordinateRows(const string &in filename)
	{
		jjSTREAM load(filename);
		int rows = 0;
		while(!load.isEmpty()){
			string str = "";
			load.getLine(str);
			array<string> parts = str.split(" ");
			if(parts.length >= 5)
				rows++;
		}
		return rows;
	}
	int getMappedEditAnimID(int charSlot, int animID)
	{
		int mappedAnim = animID;

		if(charSlot >= 0
		&& charSlot < int(CharsNew::chars.length)
		&& animID >= 0
		&& animID < int(CharsNew::chars[charSlot].animRel.length))
		{
			int candidate = CharsNew::chars[charSlot].animRel[animID];

			if(candidate >= 0 && candidate < ANIMS_PER_SET)
				mappedAnim = candidate;
		}

		return mappedAnim;
	}

	bool hasMappedEditorAnimList(int charSlot)
	{
		return charSlot >= 0
			&& charSlot < int(CharsNew::chars.length)
			&& CharsNew::chars[charSlot].animRel.length > 0;
	}

	bool hasSingleEditorAnimTarget(int charSlot)
	{
		return isHeadSlot(charSlot)
			&& charSlot >= 0
			&& charSlot < int(CharsNew::chars.length)
			&& CharsNew::chars[charSlot].LivesAnim >= 0;
	}

	bool hasFixedEditorAnimTargets(int charSlot)
	{
		return charSlot == CharsNew::CharAnim::WITCH
			|| charSlot == CharsNew::CharAnim::EVA
			|| hasSingleEditorAnimTarget(charSlot);
	}

	bool editorAnimTargetExists(array<int>@ targets, int target)
	{
		for(uint i = 0; i < targets.length; i++)
			if(targets[i] == target) return true;
		return false;
	}

	void appendSpazBirdEditorTargets(int charSlot, array<int>@ targets)
	{
		if(charSlot != CharsNew::CharAnim::SPAZ) return;

		for(int animID = SPAZ_BIRD_ANIM_FIRST; animID <= SPAZ_BIRD_ANIM_LAST; animID++)
		{
			if(!editorAnimTargetExists(targets, animID))
				targets.insertLast(animID);
		}
	}

	void appendHangLedgeEditorTarget(int charSlot, array<int>@ targets)
	{
		if(!isHangLedgeAccessorySlot(charSlot)) return;
		for(int i = int(targets.length) - 1; i >= 0; i--)
		{
			if(targets[i] == CharsNew::HANG_LEDGE_EDITOR_ANIM)
				targets.removeAt(i);
		}
		targets.insertLast(CharsNew::HANG_LEDGE_EDITOR_ANIM);
	}

	void buildEditorAnimTargets(int charSlot, array<int>@ targets)
	{
		targets.resize(0);
		if(charSlot == CharsNew::CharAnim::WITCH){
			targets.insertLast(0);
			targets.insertLast(1);
			targets.insertLast(2);
			return;
		}
		if(charSlot == CharsNew::CharAnim::EVA){
			targets.insertLast(0);
			targets.insertLast(2);
			targets.insertLast(3);
			targets.insertLast(5);
			targets.insertLast(6);
			return;
		}
		if(hasSingleEditorAnimTarget(charSlot)){
			targets.insertLast(CharsNew::chars[charSlot].LivesAnim);
			return;
		}
		if(!hasMappedEditorAnimList(charSlot))
		{
			if(isHangLedgeAccessorySlot(charSlot))
			{
				int count = getAnimCountForSet(editorAnimationForSlot(charSlot));
				if(count < 0) count = 0;
				for(int i = 0; i < count; i++)
				{
					if(i != CharsNew::HANG_LEDGE_EDITOR_ANIM)
						targets.insertLast(i);
				}
				appendSpazBirdEditorTargets(charSlot, targets);
				appendHangLedgeEditorTarget(charSlot, targets);
			}
			return;
		}
		for(uint i = 0; i < CharsNew::chars[charSlot].animRel.length; i++){
			int target = getMappedEditAnimID(charSlot, int(i));
			if(target < 0 || target >= ANIMS_PER_SET) continue;
			if(!editorAnimTargetExists(targets, target))
				targets.insertLast(target);
		}
		appendSpazBirdEditorTargets(charSlot, targets);
		targets.sortAsc();
		appendHangLedgeEditorTarget(charSlot, targets);
	}

	int firstEditorAnimID()
	{
		array<int> targets;
		buildEditorAnimTargets(selectedCharacterSlot, @targets);
		if(targets.length > 0) return targets[0];
		return 0;
	}

	int clampEditorAnimID(int value)
	{
		if(!hasMappedEditorAnimList(selectedCharacterSlot) && !hasFixedEditorAnimTargets(selectedCharacterSlot) && !isHangLedgeAccessorySlot(selectedCharacterSlot))
		{
			int count = getAnimCountForSet(animation);
			if(value < 0) return 0;
			if(count <= 0) return 0;
			if(value >= count) return count - 1;
			return value;
		}

		array<int> targets;
		buildEditorAnimTargets(selectedCharacterSlot, @targets);
		if(targets.length == 0) return 0;
		for(uint i = 0; i < targets.length; i++)
		{
			if(targets[i] == value) return value;
		}
		for(uint i = 0; i < targets.length; i++){
			if(targets[i] > value) return targets[i];
		}
		return targets[int(targets.length) - 1];
	}

	int stepEditorAnimID(int current, int delta)
	{
		if(!hasMappedEditorAnimList(selectedCharacterSlot) && !hasFixedEditorAnimTargets(selectedCharacterSlot) && !isHangLedgeAccessorySlot(selectedCharacterSlot))
			return clampEditorAnimID(current + delta);

		array<int> targets;
		buildEditorAnimTargets(selectedCharacterSlot, @targets);
		if(targets.length == 0) return 0;
		int index = 0;
		bool foundCurrent = false;
		for(uint i = 0; i < targets.length; i++){
			if(targets[i] == current) { index = int(i); foundCurrent = true; break; }
			if(!foundCurrent && targets[i] < current) index = int(i);
		}
		if(!foundCurrent && isHangLedgeAccessorySlot(selectedCharacterSlot) && current > CharsNew::HANG_LEDGE_EDITOR_ANIM && targets.length >= 2)
			index = int(targets.length) - 2;
		index += delta;
		if(index < 0) index = 0;
		if(index >= int(targets.length)) index = int(targets.length) - 1;
		return targets[index];
	}

	int getAnimCountForSet(int setID)
	{
		if(selectedCharacterSlot == CharsNew::CharAnim::DEVAN)
			return int(CharsNew::DevanAnim::WARP_OUT_GUN) + 1;
		int minCount = CharsNew::GetCharactersMinAnimCount(setID);
		if(minCount > 0)
			return minCount;
		if(setID < 0 || setID >= MAX_ANIM_SETS)
			return ANIMS_PER_SET;
		int start = jjAnimSets[setID].firstAnim;
		int limit = -1;
		for(int i = 0; i < MAX_ANIM_SETS; i++){
			int nextStart = jjAnimSets[i].firstAnim;
			if(nextStart > start && (limit < 0 || nextStart < limit))
				limit = nextStart;
		}
		if(limit < 0)
			limit = start + ANIMS_PER_SET;
		int count = limit - start;
		return count > 0 ? count : ANIMS_PER_SET;
	}

	int getDevanEditorFrameCount(int animID)
	{
		switch(animID)
		{
			case CharsNew::DevanAnim::DEVIL_FIRE_BREATH: return int(CharsNew::numFrames(CharsNew::DevanAnim::DEVIL_FIRE_BREATH));
			case CharsNew::DevanAnim::STARS: return int(CharsNew::numFrames(CharsNew::DevanAnim::STARS));
			case CharsNew::DevanAnim::FALLING: return int(CharsNew::numFrames(CharsNew::DevanAnim::FALLING));
			case CharsNew::DevanAnim::STARS_SQUISH: return int(CharsNew::numFrames(CharsNew::DevanAnim::STARS_SQUISH));
			case CharsNew::DevanAnim::FIREBALL: return int(CharsNew::numFrames(CharsNew::DevanAnim::FIREBALL));
			case CharsNew::DevanAnim::DEVIL_FLY: return int(CharsNew::numFrames(CharsNew::DevanAnim::DEVIL_FLY));
			case CharsNew::DevanAnim::DEVIL_TRANSFORM: return int(CharsNew::numFrames(CharsNew::DevanAnim::DEVIL_TRANSFORM));
			case CharsNew::DevanAnim::DEVIL_REVERT: return int(CharsNew::numFrames(CharsNew::DevanAnim::DEVIL_REVERT));
			case CharsNew::DevanAnim::IDLE: return int(CharsNew::numFrames(CharsNew::DevanAnim::IDLE));
			case CharsNew::DevanAnim::DEVIL_TURN: return int(CharsNew::numFrames(CharsNew::DevanAnim::DEVIL_TURN));
			case CharsNew::DevanAnim::WARP_IN: return int(CharsNew::numFrames(CharsNew::DevanAnim::WARP_IN));
			case CharsNew::DevanAnim::WARP_OUT: return int(CharsNew::numFrames(CharsNew::DevanAnim::WARP_OUT));
			case CharsNew::DevanAnim::BLASTER_DISSAPEAR: return int(CharsNew::numFrames(CharsNew::DevanAnim::BLASTER_DISSAPEAR));
			case CharsNew::DevanAnim::DUCK: return int(CharsNew::numFrames(CharsNew::DevanAnim::DUCK));
			case CharsNew::DevanAnim::FIRE: return int(CharsNew::numFrames(CharsNew::DevanAnim::FIRE));
			case CharsNew::DevanAnim::BLASTER: return int(CharsNew::numFrames(CharsNew::DevanAnim::BLASTER));
			case CharsNew::DevanAnim::RUN_JUMP: return int(CharsNew::numFrames(CharsNew::DevanAnim::RUN_JUMP));
			case CharsNew::DevanAnim::BLASTERBULLET: return int(CharsNew::numFrames(CharsNew::DevanAnim::BLASTERBULLET));
			case CharsNew::DevanAnim::RUN: return int(CharsNew::numFrames(CharsNew::DevanAnim::RUN));
			case CharsNew::DevanAnim::RAISE_GUN: return int(CharsNew::numFrames(CharsNew::DevanAnim::RAISE_GUN));
			case CharsNew::DevanAnim::FALL: return int(CharsNew::numFrames(CharsNew::DevanAnim::FALL));
			case CharsNew::DevanAnim::IDLE_GUN: return int(CharsNew::numFrames(CharsNew::DevanAnim::IDLE_GUN));
			case CharsNew::DevanAnim::WARP_IN_GUN: return int(CharsNew::numFrames(CharsNew::DevanAnim::WARP_IN_GUN));
			case CharsNew::DevanAnim::WARP_OUT_GUN: return int(CharsNew::numFrames(CharsNew::DevanAnim::WARP_OUT_GUN));
		}
		return 1;
	}

	int getCurrentAnimFrameCount()
	{
		if(isSpazBirdEditorAnim(selectedCharacterSlot, anim_id))
		{
			int count = jjAnimations[
				jjAnimSets[ANIM::SPAZ2].firstAnim + spazBirdLocalAnimID(anim_id)
			].frameCount;
			return count > 0 ? count : 1;
		}
		if(anim_id == CharsNew::HANG_LEDGE_EDITOR_ANIM && isHangLedgeAccessorySlot(selectedCharacterSlot))
		{
			int count = jjAnimations[jjAnimSets[ledgesEditorAnimSetForSlot(selectedCharacterSlot)].firstAnim + ledgesEditorAnimIDForSlot(selectedCharacterSlot)].frameCount;
			return count > 0 ? count : 1;
		}
		if(selectedCharacterSlot == CharsNew::CharAnim::DEVAN)
		{
			int count = getDevanEditorFrameCount(anim_id);
			return count > 0 ? count : 1;
		}
		int sourceSet = -1;
		if(selectedCharacterSlot >= 0 && selectedCharacterSlot < int(CharsNew::chars.length))
			sourceSet = sourceSetForFrameSlot(selectedCharacterSlot);
		int currentAnimation = editorAnimationForAnimID(selectedCharacterSlot, anim_id);

		if(sourceSet >= 0
		&& sourceSet < int(CharsNew::charactersMinAnimCount.length)
		&& anim_id >= 0
		&& anim_id < int(CharsNew::charactersMinAnimCount[sourceSet].length))
		{
			int minCount = CharsNew::charactersMinAnimCount[sourceSet][anim_id];
			if(minCount > 0)
				return minCount;
		}

		if(currentAnimation < 0 || anim_id < 0)
			return 1;
		int count = jjAnimations[jjAnimSets[currentAnimation].firstAnim + anim_id].frameCount;
		return count > 0 ? count : 1;
	}

	void clampCurrentFrame()
	{
		int frameCount = getCurrentAnimFrameCount();
		if(frameCount <= 0)
			frameCount = 1;
		if(frame_id >= uint(frameCount))
			frame_id = uint(frameCount - 1);
	}

	void changeEditorAnim(int delta)
	{
		saveEdit();
		anim_id = stepEditorAnimID(anim_id, delta);
		frame_id = 0;
		adjustForCurrentFrame();
		saveFile();
	}

	void changeEditorFrame(int delta)
	{
		clampCurrentFrame();
		saveEdit();
		int frameCount = getCurrentAnimFrameCount();
		int nextFrame = int(frame_id) + delta;
		if(nextFrame >= frameCount)
		{
			int nextAnim = stepEditorAnimID(anim_id, 1);
			if(nextAnim != anim_id)
			{
				anim_id = nextAnim;
				frame_id = 0;
			}
			else frame_id = uint(frameCount - 1);
		}
		else if(nextFrame < 0)
		{
			int previousAnim = stepEditorAnimID(anim_id, -1);
			if(previousAnim != anim_id)
			{
				anim_id = previousAnim;
				frame_id = uint(getCurrentAnimFrameCount() - 1);
			}
			else frame_id = 0;
		}
		else frame_id = uint(nextFrame);
		adjustForCurrentFrame();
		saveFile();
	}

	void changeEditorCharSlot(int delta)
	{
		saveEdit();
		setEditCharacterSlot(selectedCharacterSlot + delta);
		saveFile();
	}

	void changeEditorCategory(int delta)
	{
		saveEdit();
		saveFile();
		int categoryCount = int(AccessoryCategoryNames.length);
		editCategory += delta;
		while(editCategory < 0) editCategory += categoryCount;
		while(editCategory >= categoryCount) editCategory -= categoryCount;
		currentCategoryLoaded = false;
		readFile();
		use = uint(int(use) % accessoryFrameCount(editCategory));
		adjustForCurrentFrame();
	}

	void saveCurrentEditorAdjustment()
	{
		saveEdit();
	}

	void flushCurrentEditorAdjustment()
	{
		saveEdit();
		saveFile();
	}

	void adjustForCurrentFrame(){
		clampCurrentFrame();
		int frameIndex = frameIndexForAnimFrame(editorAnimationForAnimID(selectedCharacterSlot, anim_id), anim_id, frame_id, selectedCharacterSlot);
		if(frameIndex < 0) return;
		chosen_frame = getPositionIndex(coordinateSlotForEditor(selectedCharacterSlot), frameIndex);
		//jjAlert(chosen_frame+"");
		if(edited[chosen_frame] == 1){ //this is so that it does not reset to 0,0 for unedited positions
			adjX = adjustX[chosen_frame];
			adjY = adjustY[chosen_frame];
			rot = rotation[chosen_frame];
			adjScaleX = scaleX[chosen_frame];
			adjScaleY = scaleY[chosen_frame];
			//use = using[chosen_frame];
		}
	}
	void copyPreviousFrame(){
		clampCurrentFrame();
		int frameIndex = frameIndexForAnimFrame(editorAnimationForAnimID(selectedCharacterSlot, anim_id), anim_id, frame_id, selectedCharacterSlot);
		if(frameIndex < 0) return;
		int coordinateSlot = coordinateSlotForEditor(selectedCharacterSlot);
		chosen_frame = getPositionIndex(coordinateSlot, frameIndex);
		if(frameIndex - 1 > 0){
			int previous = getPositionIndex(coordinateSlot, frameIndex - 1);
			adjX = adjustX[previous];
			adjY = adjustY[previous];
			rot = rotation[previous];
			adjScaleX = scaleX[previous];
			adjScaleY = scaleY[previous];
		}
	}
	int increment = 1, rotationIncrement = 10;
	bool pressedFire = false;
	string formatHatScale(float value)
	{
		bool negative = value < 0;
		float absolute = negative ? -value : value;
		int tenths = int(absolute * 10 + 0.5);
		int whole = tenths / 10;
		int frac = tenths - whole * 10;
		return (negative ? "-" : "") + whole + "." + frac;
	}

	bool isHatNumericField(int action)
	{
		return action == HAT_ACTION_ANIM
			|| action == HAT_ACTION_FRAME
			|| action == HAT_ACTION_CHAR
			|| action == HAT_ACTION_POS_X
			|| action == HAT_ACTION_POS_Y
			|| action == HAT_ACTION_ROTATION
			|| action == HAT_ACTION_SCALE_X
			|| action == HAT_ACTION_SCALE_Y
			|| action == HAT_ACTION_ADDITIONAL_HAT;
	}

	bool isHatFloatField(int action)
	{
		return action == HAT_ACTION_SCALE_X || action == HAT_ACTION_SCALE_Y;
	}

	string hatFieldValueText(int action)
	{
		if(action == HAT_ACTION_ANIM) return "" + anim_id;
		if(action == HAT_ACTION_FRAME) return "" + frame_id;
		if(action == HAT_ACTION_CHAR) return "" + selectedCharacterSlot;
		if(action == HAT_ACTION_POS_X) return "" + adjX;
		if(action == HAT_ACTION_POS_Y) return "" + adjY;
		if(action == HAT_ACTION_ROTATION) return "" + rot;
		if(action == HAT_ACTION_SCALE_X) return formatHatScale(adjScaleX);
		if(action == HAT_ACTION_SCALE_Y) return formatHatScale(adjScaleY);
		if(action == HAT_ACTION_ADDITIONAL_HAT) return "" + additionalAccessory;
		return "";
	}

	void clampAdditionalAccessory()
	{
		int count = accessoryFrameCount(editCategory);
		if(additionalAccessory < -1) additionalAccessory = -1;
		if(count <= 0) additionalAccessory = -1;
		else if(additionalAccessory >= count) additionalAccessory = count - 1;
	}

	void setHatFieldValue(int action, const string &in value)
	{
		if(value == "" || value == "-" || value == "." || value == "-.") return;
		if(action == HAT_ACTION_ANIM)
		{
			saveEdit();
			anim_id = clampEditorAnimID(parseInt(value));
			frame_id = 0;
			adjustForCurrentFrame();
			saveFile();
			return;
		}
		if(action == HAT_ACTION_FRAME)
		{
			saveEdit();
			int newFrame = parseInt(value);
			if(newFrame < 0) newFrame = 0;
			frame_id = uint(newFrame);
			clampCurrentFrame();
			adjustForCurrentFrame();
			saveFile();
			return;
		}
		if(action == HAT_ACTION_CHAR)
		{
			saveEdit();
			setEditCharacterSlot(parseInt(value));
			saveFile();
			return;
		}
		if(action == HAT_ACTION_POS_X) adjX = parseInt(value);
		else if(action == HAT_ACTION_POS_Y) adjY = parseInt(value);
		else if(action == HAT_ACTION_ROTATION) rot = parseInt(value);
		else if(action == HAT_ACTION_SCALE_X) adjScaleX = parseFloat(value);
		else if(action == HAT_ACTION_SCALE_Y) adjScaleY = parseFloat(value);
		else if(action == HAT_ACTION_ADDITIONAL_HAT)
		{
			additionalAccessory = parseInt(value);
			clampAdditionalAccessory();
		}
		else return;
		if(action == HAT_ACTION_ADDITIONAL_HAT) return;
		saveCurrentEditorAdjustment();
	}

	void beginHatFieldEdit(int action)
	{
		if(!isHatNumericField(action)) return;
		if(hatEditField >= 0 && hatEditField != action)
			commitHatFieldEdit();
		hatEditField = action;
		hatEditValue = hatFieldValueText(action);
		hatEditFresh = true;
	}

	void commitHatFieldEdit()
	{
		if(hatEditField < 0) return;
		setHatFieldValue(hatEditField, hatEditValue);
		hatEditField = -1;
		hatEditValue = "";
		hatEditFresh = false;
	}

	void cancelHatFieldEdit()
	{
		hatEditField = -1;
		hatEditValue = "";
		hatEditFresh = false;
	}

	bool hatTextHasDot()
	{
		for(uint i = 0; i < hatEditValue.length; i++)
			if(hatEditValue.substr(i, 1) == ".") return true;
		return false;
	}

	void appendHatEditChar(const string &in value)
	{
		if(hatEditFresh)
		{
			hatEditValue = "";
			hatEditFresh = false;
		}
		if(value == "-")
		{
			if(int(hatEditValue.length) > 0 && hatEditValue.substr(0, 1) == "-")
				hatEditValue = hatEditValue.substr(1);
			else
				hatEditValue = "-" + hatEditValue;
			return;
		}
		if(value == ".")
		{
			if(!isHatFloatField(hatEditField) || hatTextHasDot()) return;
		}
		hatEditValue += value;
	}

	string hatTextWithEditValue(const string &in text, int action)
	{
		if(hatEditField != action) return text;
		int colon = int(text.findFirst(":"));
		if(colon >= 0) return text.substr(0, colon + 2) + hatEditValue;
		return hatEditValue;
	}

	string additionalAccessoryLabel()
	{
		if(editCategory == AccessoryCategory::SCARFS) return "Additional Scarfs: " + additionalAccessory;
		if(editCategory == AccessoryCategory::GLASSES) return "Additional Glasses: " + additionalAccessory;
		return "Additional Hats: " + additionalAccessory;
	}

	void drawHatStepper(jjCANVAS@ canvas, int x, int y, int textW, int action, int delta, const string &in text, bool selected = false)
	{
		int actualTextW = textW;
		string shownText = hatTextWithEditValue(text, action);
		int measuredTextW = jjGetStringWidth(shownText, STRING::SMALL, STRING::NORMAL) + 8;
		if(measuredTextW > actualTextW)
			actualTextW = measuredTextW;
		DbDrawButton(canvas, @hatEditorWindow, x, y, 16, 16, action, 0, -delta, "<");
		if(isHatNumericField(action))
			canvas.drawRectangle(x + 18, y - 11, actualTextW, 17, hatEditField == action ? 80 : 0, SPRITE::TRANSLUCENT);
		if(selected || hatEditField == action)
			canvas.drawString(x + 20, y, shownText, STRING::SMALL, STRING::PALSHIFT, hatEditField == action ? 80 : 24);
		else
			canvas.drawString(x + 20, y, shownText, STRING::SMALL, STRING::NORMAL);
		if(isHatNumericField(action))
		{
			DbAddControl(@hatEditorWindow, x + 18, y, actualTextW, 16, HAT_ACTION_EDIT_FIELD, 0, action, shownText);
		}
		else
			DbAddControl(@hatEditorWindow, x + 18, y, actualTextW, 16, action, 0, delta, shownText);
		DbDrawButton(canvas, @hatEditorWindow, x + 22 + actualTextW, y, 16, 16, action, 0, delta, ">");
	}

	int hatStepperWidth(int textW, const string &in text)
	{
		int actualTextW = textW;
		int measuredTextW = jjGetStringWidth(text, STRING::SMALL, STRING::NORMAL) + 8;
		if(measuredTextW > actualTextW)
			actualTextW = measuredTextW;
		return actualTextW + 38;
	}

	void applyHatEditorAction(int action, int delta)
	{
		if(hatEditField >= 0 && action != HAT_ACTION_EDIT_FIELD)
			commitHatFieldEdit();
		if(action == HAT_ACTION_INCREMENT)
		{
			increment = increment == 1 ? 10 : 1;
			return;
		}
		if(action == HAT_ACTION_ROT_INCREMENT)
		{
			rotationIncrement = rotationIncrement == 1 ? 10 : 1;
			return;
		}
		if(action == HAT_ACTION_CATEGORY)
		{
			changeEditorCategory(delta);
			clampAdditionalAccessory();
			return;
		}
		if(action == HAT_ACTION_ANIM)
		{
			choice = 1;
			changeEditorAnim(delta);
			return;
		}
		if(action == HAT_ACTION_FRAME)
		{
			choice = 0;
			changeEditorFrame(delta);
			return;
		}
		if(action == HAT_ACTION_CHAR)
		{
			changeEditorCharSlot(delta);
			return;
		}
		if(action == HAT_ACTION_POS_X)
		{
			adjX += delta * increment;
			saveCurrentEditorAdjustment();
			return;
		}
		if(action == HAT_ACTION_POS_Y)
		{
			adjY += delta * increment;
			saveCurrentEditorAdjustment();
			return;
		}
		if(action == HAT_ACTION_ROTATION)
		{
			rot += delta * rotationIncrement;
			saveCurrentEditorAdjustment();
			return;
		}
		if(action == HAT_ACTION_SCALE_X)
		{
			adjScaleX += delta * (increment == 1 ? 0.1 : 1);
			saveCurrentEditorAdjustment();
			return;
		}
		if(action == HAT_ACTION_SCALE_Y)
		{
			adjScaleY += delta * (increment == 1 ? 0.1 : 1);
			saveCurrentEditorAdjustment();
			return;
		}
		if(action == HAT_ACTION_MOUSE)
		{
			hatMouseMode = !hatMouseMode;
			return;
		}
		if(action == HAT_ACTION_ADDITIONAL_HAT)
		{
			additionalAccessory += delta;
			clampAdditionalAccessory();
			return;
		}
		if(action == HAT_ACTION_EDIT_FIELD)
		{
			beginHatFieldEdit(delta);
		}
	}

	int currentHatEditKey()
	{
		for(int key = 0x30; key <= 0x39; key++)
			if(jjKey[key]) return key;
		for(int key = 0x60; key <= 0x69; key++)
			if(jjKey[key]) return key;
		if(jjKey[0xBD] || jjKey[0x6D]) return 0xBD;
		if(jjKey[0xBE] || jjKey[0x6E]) return 0xBE;
		if(jjKey[0x08]) return 0x08;
		if(jjKey[0x0D]) return 0x0D;
		if(jjKey[0x1B]) return 0x1B;
		return -1;
	}

	bool processHatTextInput()
	{
		if(hatEditField < 0) return false;
		int key = currentHatEditKey();
		if(key < 0)
		{
			hatEditLastKey = -1;
			return true;
		}
		if(key == hatEditLastKey) return true;
		hatEditLastKey = key;

		if(key >= 0x30 && key <= 0x39)
			appendHatEditChar("" + (key - 0x30));
		else if(key >= 0x60 && key <= 0x69)
			appendHatEditChar("" + (key - 0x60));
		else if(key == 0xBD)
			appendHatEditChar("-");
		else if(key == 0xBE)
			appendHatEditChar(".");
		else if(key == 0x08)
		{
			if(hatEditFresh)
			{
				hatEditValue = "";
				hatEditFresh = false;
			}
			else if(int(hatEditValue.length) > 0)
				hatEditValue = hatEditValue.substr(0, int(hatEditValue.length) - 1);
		}
		else if(key == 0x0D)
			commitHatFieldEdit();
		else if(key == 0x1B)
			cancelHatFieldEdit();
		return true;
	}

	void drawHatEditorPanel(jjPLAYER@ p, jjCANVAS@ canvas)
	{
		DbBegin(@hatEditorWindow, "charsNewHatEditor", "Hat Editor", jjResolutionWidth - 432, 72, 422, 280);
		hatEditorWindow.draggable = true;
		hatEditorWindow.closeButton = true;
		hatEditorWindow.titleBar = true;
		hatEditorWindow.backgroundColor = 32;
		hatEditorWindow.titleColor = 24;
		hatEditorWindow.focusColor = 15;
		DbDrawFrame(canvas, @hatEditorWindow);

		int x = hatEditorWindow.x + 10;
		int y = hatEditorWindow.y + 32;
		drawHatStepper(canvas, x, y, 110, HAT_ACTION_CATEGORY, 1, "Category: " + currentCategoryName());
		y += 20;
		drawHatStepper(canvas, x, y, 96, HAT_ACTION_INCREMENT, 1, "Increment: " + increment);
		drawHatStepper(canvas, x + 180, y, 86, HAT_ACTION_ROT_INCREMENT, 1, "Rot Inc: " + rotationIncrement);
		y += 20;
		drawHatStepper(canvas, x, y, 90, HAT_ACTION_ANIM, 1, "Anim ID: " + anim_id, choice == 1);
		drawHatStepper(canvas, x + 205, y, 100, HAT_ACTION_FRAME, 1, "Frame ID: " + frame_id, choice == 0);
		y += 20;
		drawHatStepper(canvas, x, y, 110, HAT_ACTION_CHAR, 1, "Char slot: " + selectedCharacterSlot);
		y += 22;
		canvas.drawString(x, y, "Position:", STRING::SMALL, STRING::NORMAL);
		y += 17;
		string posXText = "X: " + adjX;
		drawHatStepper(canvas, x, y, 70, HAT_ACTION_POS_X, 1, posXText);
		drawHatStepper(canvas, x + hatStepperWidth(70, posXText) + 14, y, 78, HAT_ACTION_POS_Y, 1, "Y: " + adjY);
		y += 22;
		canvas.drawString(x, y, "Rotation:", STRING::SMALL, STRING::NORMAL);
		y += 17;
		drawHatStepper(canvas, x, y, 70, HAT_ACTION_ROTATION, 1, "" + rot);
		y += 22;
		canvas.drawString(x, y, "Size:", STRING::SMALL, STRING::NORMAL);
		y += 17;
		string scaleXText = "X: " + formatHatScale(adjScaleX);
		drawHatStepper(canvas, x, y, 82, HAT_ACTION_SCALE_X, 1, scaleXText);
		drawHatStepper(canvas, x + hatStepperWidth(82, scaleXText) + 14, y, 82, HAT_ACTION_SCALE_Y, 1, "Y: " + formatHatScale(adjScaleY));
		y += 24;
		DbDrawButton(canvas, @hatEditorWindow, x, y, 112, 16, HAT_ACTION_MOUSE, 0, 0, "Mouse: " + (hatMouseMode ? "ON" : "OFF"), true, hatMouseMode);
		drawHatStepper(canvas, x + 132, y, 150, HAT_ACTION_ADDITIONAL_HAT, 1, additionalAccessoryLabel());

		DbResetResult(@hatEditorResult);
		DbProcessMouse(@hatEditorWindow, @hatEditorResult);
		if(hatEditorResult.closeRequested)
		{
			flushCurrentEditorAdjustment();
			EDITING_MODE = false;
		}
		else if(hatEditorResult.activated)
			applyHatEditorAction(hatEditorResult.action, hatEditorResult.b);
	}

	void drawHatInstructionsBox(jjCANVAS@ canvas)
	{
		if(!SHOW_HAT_INSTRUCTIONS) return;
		array<string> lines = {
			"!hatInstructions on/off toggles this help",
			"Up/Down: next/previous frame or anim",
			"W/A/S/D: move accessory, Q/E: rotate",
			"Mouse ON: click preview to place",
			"Jump: toggle increment 1/10, Fire: copy previous frame",
			"!hatBase jazz/spaz/lori/frog edits base chars",
			"!hatChar id/name edits slot, e.g. !hatChar mario, witch, eggman",
			"!hatGroup hat/bowtie/glasses sets accessory type",
			"!hatColorOffsetRed/Y/Bk/B/G, !scarfColorOffset, !glassesColorOffset"
		};
		int x = 5;
		int y = 392;
		int w = 0;
		for(uint i = 0; i < lines.length; i++){
			int lineW = jjGetStringWidth(lines[i], STRING::SMALL, STRING::NORMAL);
			if(lineW > w) w = lineW;
		}
		w += 24;
		if(w > jjResolutionWidth - 10)
			w = jjResolutionWidth - 10;
		int h = 20 + int(lines.length) * 18;
		canvas.drawRectangle(x, y, w, h, 0, SPRITE::TRANSLUCENT);
		canvas.drawRectangle(x, y, w, h, 0, SPRITE::SHADOW);
		int lineY = y + 12;
		for(uint i = 0; i < lines.length; i++){
			canvas.drawString(x + 8, lineY, lines[i], STRING::SMALL, STRING::NORMAL);
			lineY += 18;
		}
	}

	bool hatMouseOverEditor()
	{
		return DbPointIn(jjMouseX, jjMouseY + 8, hatEditorWindow.x, hatEditorWindow.y, hatEditorWindow.w, hatEditorWindow.h);
	}

	void handleHatMouseEditing(jjPLAYER@ play)
	{
		if(!hatMouseMode)
		{
			hatMouseWasDown = false;
			return;
		}

[preview ends here]