Downloads containing npcBunniesHookCore.asc

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

File preview

namespace npcBunniesHooks
{
bool doneSetup = false;
bool requestedNpcServerSync = false;
bool requestedNpcSyncSetting = false;
bool receivedNpcSyncSetting = false;

void onLevelLoad()
{
	npcBunniesCore::LoadNpcBunniesPreferences();
	if(jjIsServer) npcBunniesCore::LoadNpcSyncSetting();
	npcBunniesAnimations::LoadCharactersModAnimationIndexes();
	//npcList.length + 1: because we have all characters and NpcAnim::HEAD
	//the empty folders in charactersMod.j2a should theoretically leave ANIM::CUSTOM empty
	//TODO: debug FurSetup_AdditionalCharacters to see if it's really like that
	uint charCount = npcBunniesCore::npcList.length + 1;
	npcBunniesAnimations::emptyCharSet.resize(charCount);
	npcBunniesAnimations::animTypes.resize(charCount);

	for(uint i=0; i<charCount; i++)
	{
		npcBunniesAnimations::FurSetup_AdditionalCharacters(i);
	}
	npcBunniesAccessories::NpcAccessories::Load();
	npcBunniesParser::LoadNpcDialogueFile();
	if(npcBunniesCore::LOAD_BIG_NPC_ANIMATIONS) npcBunniesAnimations::loadMenuAnims();
	if(npcBunniesCore::LOAD_ALL_MONSTER_ANIMATIONS) npcBunniesAnimations::LoadNpcUiIconAnimSets();

	npcBunniesMovement::readLevelMap();
	doneSetup = false;
	requestedNpcServerSync = false;
	requestedNpcSyncSetting = jjIsServer;
	receivedNpcSyncSetting = jjIsServer;
	if(jjIsServer) sendNpcSyncSetting(0);
}


void onMain()
{
	npcBunniesAccessories::NpcAccessories::OnMain();
	npcBunniesEditorCore::NpcDesigner_OnMain();
	if(!doneSetup)
	{
		doneSetup = true;
		//importSettings(); - will import settings from the _npc.asdat file
	}
	if(!jjIsServer && !requestedNpcSyncSetting)
	{
		requestNpcSyncSetting();
		requestedNpcSyncSetting = true;
	}
	if(receivedNpcSyncSetting && npcBunniesCore::ENABLE_NPC_SYNC && !jjIsServer && !requestedNpcServerSync)
	{
		requestNpcServerSync();
		requestedNpcServerSync = true;
	}

	requestNpcBunniesAdminAccess();
	npcBunniesCore::isChatOpen();
	npcBunniesCore::ResetInactivePlayerQuestState();
	ProcessNpcQuestDialogInput();
	if(npcBunniesFighting::HasAnyNpcFightingEnabled()) npcBunniesFighting::UpdateNpcFightingPlayerFireState();
	if(npcBunniesCore::ENABLE_NPC_MONSTER_QUESTS) npcBunniesQuests::UpdateTrackedDefeatObjects();
}

array<string> NpcDialogueWrappedMediumLines(const string &in text, int maxPixels)
{
	array<string> lines;
	if(maxPixels <= 0) return lines;
	array<string> paragraphs = text.split("\n");
	for(uint p = 0; p < paragraphs.length; p++)
	{
		if(paragraphs[p] == "")
		{
			lines.insertLast("");
			continue;
		}
		array<string> words = paragraphs[p].split(" ");
		string line = "";
		for(uint i = 0; i < words.length; i++)
		{
			string remaining = words[i];
			while(remaining != "" && jjGetStringWidth(remaining, STRING::MEDIUM, STRING::NORMAL) > maxPixels)
			{
				int take = int(remaining.length());
				while(take > 1 && jjGetStringWidth(remaining.substr(0, take), STRING::MEDIUM, STRING::NORMAL) > maxPixels)
					take--;
				if(line != "")
				{
					lines.insertLast(line);
					line = "";
				}
				lines.insertLast(remaining.substr(0, take));
				remaining = remaining.substr(take);
			}
			if(remaining == "") continue;
			string candidate = line == "" ? remaining : line + " " + remaining;
			if(jjGetStringWidth(candidate, STRING::MEDIUM, STRING::NORMAL) <= maxPixels) line = candidate;
			else
			{
				if(line != "") lines.insertLast(line);
				line = remaining;
			}
		}
		if(line != "") lines.insertLast(line);
	}
	if(lines.length == 0) lines.insertLast("");
	return lines;
}

void DrawNpcDialogueScrollableText(jjCANVAS@ canvas, const string &in text, int x, int y, int w, int maxY)
{
	array<string> lines = NpcDialogueWrappedMediumLines(text, w);
	int textY = y;
	int textMaxY = maxY;
	int maxLineCount = int(lines.length);
	int plainVisibleLines = (maxY - y) / 20;
	if(plainVisibleLines < 1) plainVisibleLines = 1;
	bool showScrollButtons = maxLineCount > plainVisibleLines;
	if(showScrollButtons)
	{
		textY = y + 16;
		textMaxY = maxY - 16;
	}
	int visibleLines = (textMaxY - textY) / 20;
	if(visibleLines < 1) visibleLines = 1;
	int maxScroll = int(lines.length) - visibleLines;
	if(maxScroll < 0) maxScroll = 0;
	if(npcBunniesCore::npcTalkScrollLine < 0) npcBunniesCore::npcTalkScrollLine = 0;
	if(npcBunniesCore::npcTalkScrollLine > maxScroll) npcBunniesCore::npcTalkScrollLine = maxScroll;
	if(showScrollButtons)
	{
		canvas.drawString(x + w / 2 - 12, y, "UP", STRING::SMALL, npcBunniesCore::npcTalkScrollLine > 0 ? STRING::NORMAL : STRING::PALSHIFT, npcBunniesCore::npcTalkScrollLine > 0 ? 0 : 24);
		canvas.drawString(x + w / 2 - 20, maxY - 12, "DOWN", STRING::SMALL, npcBunniesCore::npcTalkScrollLine < maxScroll ? STRING::NORMAL : STRING::PALSHIFT, npcBunniesCore::npcTalkScrollLine < maxScroll ? 0 : 24);
	}
	int drawY = textY;
	for(uint i = 0; i < lines.length; i++)
	{
		if(int(i) < npcBunniesCore::npcTalkScrollLine) continue;
		if(drawY + 20 > textMaxY) break;
		canvas.drawString(0x8000, drawY, lines[i], STRING::MEDIUM, STRING::NORMAL);
		drawY += 20;
	}
}


bool onDrawScore(jjPLAYER@ play, jjCANVAS@ canvas)
{
	npcBunniesAccessories::NpcAccessories::OnDrawScore(play, canvas);
	npcBunniesAnimations::DrawNpcCountsHud(play, canvas);
	npcBunniesDialogue::DrawNpcQuestHud(canvas);
	npcBunniesDialogue::DrawNpcQuestDialog(canvas, play);
	if(npcBunniesMovement::convOn != -1)
	{
		bool DrawNpc = true;
		npcBunniesCore::npcBunny npc = npcBunniesCore::npcBunnies[npcBunniesMovement::convOn];

		int dialogueBoxW = (jjResolutionWidth - 200) * 3 / 2;
		if(dialogueBoxW > jjResolutionWidth - 40) dialogueBoxW = jjResolutionWidth - 40;
		int dialogueBoxH = 285;
		if(dialogueBoxH > jjResolutionHeight - 40) dialogueBoxH = jjResolutionHeight - 40;
		int dialogueBoxX = (jjResolutionWidth - dialogueBoxW) / 2;
		int dialogueBoxY = (jjResolutionHeight - dialogueBoxH) / 2;
		int dialogueBigSpriteRightOffsetX = 50;
		int dialogueHeadRightOffsetX = 50;
		int dialogueTextExtraWidthLeft = 100;
		int dialoguePortraitRightX = dialogueBoxX + dialogueBoxW - 50 + dialogueBigSpriteRightOffsetX;
		int dialoguePortraitTopY = dialogueBoxY + 36;
		int dialogueSmallFaceX = dialogueBoxX + dialogueBoxW - 70 + dialogueHeadRightOffsetX;
		int dialogueSmallFaceY = dialogueBoxY + 58;
		canvas.drawRectangle(dialogueBoxX, dialogueBoxY, dialogueBoxW, dialogueBoxH, 0, SPRITE::SHADOW);
		if(npc.action == npcBunniesCore::NpcAction::Menu)
		{
			//TO DO: call drawThisNpc(canvas);
			npcBunniesDialogue::getMenuOptionControl(play);
			if(npcBunniesMovement::convOn != -1)
			{
				npc = npcBunniesCore::npcBunnies[npcBunniesMovement::convOn];

				jjTEXTAPPEARANCE ChosenAppearance(STRING::SPIN);
				jjTEXTAPPEARANCE ConversationAppearance(STRING::NORMAL);
				for(uint i=0; i<npcBunniesDialogue::npcMenuOptionsLength; i++)
				{
					int optionY = 225 + int(i) * 25;
					canvas.drawString(0x8000, optionY, npcBunniesDialogue::GetNpcMenuLabel(npc, play, int(i)), STRING::MEDIUM, npcBunniesDialogue::chosenMenuOption == int(i) ? ChosenAppearance : ConversationAppearance);
				}
				canvas.drawString(0x8000, 237 + int(npcBunniesDialogue::npcMenuOptionsLength) * 25, npcBunniesDialogue::npcMenuOptions[0], STRING::MEDIUM, npcBunniesDialogue::chosenMenuOption == int(npcBunniesDialogue::npcMenuOptionsLength) ? ChosenAppearance : ConversationAppearance);
			}
		}
		else if(npc.action == npcBunniesCore::NpcAction::Shop)
		{
			npcBunniesShop::DrawNpcShop(play, canvas, npcBunniesMovement::convOn);
		}
		else if(npc.action == npcBunniesCore::NpcAction::Guide && npc.dialogueIndex != -1)
		{
			jjTEXTAPPEARANCE ConversationAppearance(STRING::NORMAL);
			string Line = npc.guidePendingLine;
			uint CharacterCount = 0;
			string stringToWrite = Line;
			string leftOutLine = Line;
			const int dialogueResolutionOffset = 300;
			uint numberOfLines = uint(jjGetStringWidth(Line, STRING::MEDIUM, ConversationAppearance)/(jjResolutionWidth - dialogueResolutionOffset))+1;
			for(uint i=0; i<numberOfLines; i++)
			{
				if(jjGetStringWidth(leftOutLine, STRING::MEDIUM, ConversationAppearance) > jjResolutionWidth - dialogueResolutionOffset)
				{
					array<string> lineParts = leftOutLine.split(" ");
					CharacterCount = 0;
					for(uint j=0; j<lineParts.length; j++)
					{
						if(jjGetStringWidth(leftOutLine.substr(0, CharacterCount+lineParts[j].length+1), STRING::MEDIUM, ConversationAppearance) < jjResolutionWidth - dialogueResolutionOffset)
							CharacterCount+=lineParts[j].length+1;
						else break;
					}
					stringToWrite = leftOutLine.substr(0, CharacterCount);
					leftOutLine = leftOutLine.substr(CharacterCount, leftOutLine.length - CharacterCount + 1);
				}
				else stringToWrite = leftOutLine;
				canvas.drawString(0x8000, 250+i*20, stringToWrite, STRING::MEDIUM, ConversationAppearance);
			}
		}
		else if(npc.action == npcBunniesCore::NpcAction::Talk && npc.dialogueIndex != -1)
		{
			npcBunniesDialogue::DialogueSet@ portraitSet = npc.dialogueSets[npc.convIndex];
			npcBunniesDialogue::DialogueLine@ portraitLine = portraitSet.dialogue[npc.dialogueIndex];
			int frameIndex = portraitSet.bigFrame < 0 ? jjGameTicks / 10 : portraitSet.bigFrame;
			uint animIndex = npcBunniesAnimations::animTypes.find(npc.type);
			int personId = portraitLine.personId;
			npcBunniesCore::npcBunny portraitNpc = npc;
			if(portraitLine.isNpc == 0)
			{
				personId = portraitSet.speakerNpcIndex >= 0 ? portraitSet.speakerNpcIndex : npc.id;
				if(0 <= personId && personId < int(npcBunniesCore::npcBunnies.length)) portraitNpc = npcBunniesCore::npcBunnies[personId];
				else personId = npc.id;
				animIndex = npcBunniesAnimations::animTypes.find(portraitNpc.type);
			}
			else if(0 <= personId && personId < 32 && portraitLine.isNpc == 1)
			{
				animIndex = 0;
				if(jjPlayers[personId].isActive) animIndex = npcBunniesAnimations::charFind.find(jjPlayers[personId].charCurr);
				//TO DO: you could also read if mario mutator is active and read which character the player is from the .asdat file
			}
			npcBunniesAnimations::DrawParams draw;
			if(npcBunniesCore::LOAD_BIG_NPC_ANIMATIONS && animIndex <= npcBunniesCore::NpcAnim::LORI) //was 2 //TO DO: add big animations (or head animations) as chat icons for other characters
			{
				draw.add(
					xPos: animIndex == npcBunniesCore::NpcAnim::JAZZ ? (dialoguePortraitRightX - 200) : dialoguePortraitRightX,
					yPos: dialoguePortraitTopY,
					animSet: ANIM::CUSTOM[npcBunniesAnimations::customMenu[animIndex]],
					anim: 0,
					frame: frameIndex,
					sizeX: animIndex == npcBunniesCore::NpcAnim::JAZZ ? 1 : -1,
					sizeY: 1,
					spriteMode: SPRITE::PALSHIFT,
					param: personId,
					grav: 0
				);
				if(portraitLine.isNpc == 0)
				{
					array<int> params =
					{
						portraitNpc.fur.c2-24,
						portraitNpc.fur.c3-32,
						portraitNpc.fur.c4-40,
						0,
						npcBunniesCore::GetPrimaryFurShift(animIndex, portraitNpc.fur.c1),
					};
					//TODO: this for could go up to 4 and the DrawCharacterOnCanvas moved inside
					//		if the order was different in MenuAnimsMod.j2a
					draw.param = params[4];
					npcBunniesAnimations::DrawMergedCharacterLayerOnCanvas(canvas, draw, 5, 4);
					for(int i=0; i<=3; i++)
					{
						draw.anim = 0; draw.param = params[i];
						if(i == 3){ draw.spriteMode = SPRITE::NORMAL; }
						npcBunniesAnimations::DrawMergedCharacterLayerOnCanvas(canvas, draw, 5, i);
					}
				}
				else if(portraitLine.isNpc == 1)
				{
					draw.xPos = animIndex == npcBunniesCore::NpcAnim::JAZZ ? (dialoguePortraitRightX - 200) : dialoguePortraitRightX; draw.spriteMode = SPRITE::PLAYER; draw.sizeX = -draw.sizeX;
					npcBunniesAnimations::DrawMergedCharacterLayerOnCanvas(canvas, draw, 5, 4);
				}
			}
			else
			{
				int iconAnimSet = (portraitNpc.type == npcBunniesCore::NpcAnim::FROG || portraitNpc.type == npcBunniesCore::NpcAnim::BIRD) ? ANIM::FACES : ANIM::CUSTOM[npcBunniesAnimations::emptyCharSet[npcBunniesCore::NpcAnim::HEAD]]; //3 = frog or TO DO: add bird
				int charIndex = (portraitNpc.type == npcBunniesCore::NpcAnim::FROG || portraitNpc.type == npcBunniesCore::NpcAnim::BIRD) ? portraitNpc.type : npcBunniesAnimations::animTypes.find(portraitNpc.type);
				if(portraitLine.isNpc == 0 && (npcBunniesAnimations::IsNpcMonsterType(portraitNpc.type) || npcBunniesAnimations::IsNpcBossType(portraitNpc.type)))
				{
					npcBunniesAnimations::NpcUiAnimation monsterIcon;
					if(npcMonsterAdjustments::GetWalkingUiAnimation(portraitNpc.type, monsterIcon))
					{
						canvas.drawResizedSprite(dialogueSmallFaceX + monsterIcon.offsetX, dialogueSmallFaceY + monsterIcon.offsetY, monsterIcon.animSet, monsterIcon.anim, npcBunniesAnimations::AnimatedNpcUiFrame(monsterIcon), -0.75, 0.75, monsterIcon.spriteMode, monsterIcon.param);
					}
					else
					{
						npcBunniesAnimations::GetNpcSimpleAnimation(portraitNpc.type, monsterIcon);
						if(npcBunniesAnimations::CanDrawNpcSimpleAnimation(monsterIcon))
							canvas.drawResizedSprite(dialogueSmallFaceX, dialogueSmallFaceY, monsterIcon.animSet, monsterIcon.anim, npcBunniesAnimations::NpcSimpleAnimationFrame(monsterIcon), -0.75, 0.75, monsterIcon.spriteMode, monsterIcon.param);
					}
				}
				else if(portraitLine.isNpc == 0
					&& 0 <= charIndex && charIndex < int(npcBunniesAnimations::emptyCharSet.length))
				{
					animIndex = iconAnimSet == ANIM::FACES ? npcBunniesAnimations::npcFacesDefault.find(charIndex) : npcBunniesAnimations::npcFaces.find(charIndex);
					int animation = npcBunniesCore::NpcAnimations.find(RABBIT::STAND);
					bool useSmallPlayerStand = !npcBunniesCore::LOAD_BIG_NPC_ANIMATIONS && charIndex >= npcBunniesCore::NpcAnim::JAZZ && charIndex <= npcBunniesCore::NpcAnim::LORI;

					draw.add(
						xPos: dialogueSmallFaceX-npcBunniesAnimations::adjustFaceX[charIndex],
						yPos: dialogueSmallFaceY-npcBunniesAnimations::adjustFaceY[charIndex],
						animSet: useSmallPlayerStand ? ANIM::CUSTOM[npcBunniesAnimations::emptyCharSet[charIndex]] : iconAnimSet,
						anim: useSmallPlayerStand ? animation : animIndex,
						frame: jjGameTicks/12,
						sizeX: -1*npcBunniesAnimations::adjustSizeX[charIndex],
						sizeY:  1*npcBunniesAnimations::adjustSizeY[charIndex],
						spriteMode: SPRITE::PALSHIFT,
						param: 0,
						grav: 0
					);
					array<int> params =
					{
						portraitNpc.fur.c1-16,
						portraitNpc.fur.c2-24,
						portraitNpc.fur.c3-32,
						portraitNpc.fur.c4-40,
						0
					};

					if(useSmallPlayerStand)
					{
						if(charIndex == npcBunniesCore::NpcAnim::LORI)
						{
							array<int> loriParams = { npcBunniesCore::GetPrimaryFurShift(charIndex, portraitNpc.fur.c1), portraitNpc.fur.c4 - 40, 0, portraitNpc.fur.c4 - 40 };
							for(int i = 0; i < 4; i++)
							{
								if(i == 2) continue;
								if(i == 3 && npc.fur.c4 == 40) continue;
								draw.param = loriParams[i];
								npcBunniesAnimations::DrawMergedCharacterLayerOnCanvas(canvas, draw, 4, i);
							}
							draw.param = loriParams[2];
							npcBunniesAnimations::DrawMergedCharacterLayerOnCanvas(canvas, draw, 4, 2);
						}
						else
						{
							array<int> bodyParams = { npcBunniesCore::GetPrimaryFurShift(charIndex, portraitNpc.fur.c1), portraitNpc.fur.c2 - 24, portraitNpc.fur.c3 - 32, portraitNpc.fur.c4 - 40, 0 };
							for(int i = 0; i <= 4; i++)
							{
								draw.param = bodyParams[i];
								npcBunniesAnimations::DrawMergedCharacterLayerOnCanvas(canvas, draw, 5, i);
							}
						}
					}
					else
					{
						if(charIndex == npcBunniesCore::NpcAnim::FROG)
						{
							draw.param = portraitNpc.fur.c1 - 80;
							npcBunniesAnimations::DrawCharacterOnCanvas(canvas, draw);
							draw.animSet = ANIM::CUSTOM[npcBunniesAnimations::emptyCharSet[npcBunniesCore::NpcAnim::HEAD]]; draw.anim = 4; draw.spriteMode = SPRITE::NORMAL; draw.param = 0;
							npcBunniesAnimations::DrawCharacterOnCanvas(canvas, draw);
						}
						else if(charIndex == npcBunniesCore::NpcAnim::BIRD)
						{
							draw.param = portraitNpc.fur.c1 - 80;
							npcBunniesAnimations::DrawCharacterOnCanvas(canvas, draw);
							draw.animSet = ANIM::CUSTOM[npcBunniesAnimations::emptyCharSet[npcBunniesCore::NpcAnim::HEAD]]; draw.spriteMode = SPRITE::PALSHIFT;
							int start = npcBunniesAnimations::npcFaces.find(charIndex);
							for(int i=start; i<=start+2; i++)
							{
								draw.anim = i; draw.param = params[i - start + 1];
								if(i == start+2) draw.param = 0;
								npcBunniesAnimations::DrawCharacterOnCanvas(canvas, draw);
							}
							npcBunniesAnimations::DrawCharacterOnCanvas(canvas, draw);
						}
						else
						{
							for(int i=0; i<=3; i++)
							{
								if(npcBunniesAnimations::facePalshift[charIndex] == i+1)
								{
									draw.param = params[i];
									npcBunniesAnimations::DrawCharacterOnCanvas(canvas, draw);
									break;
								}
							}
						}

						if(iconAnimSet != ANIM::FACES && charIndex != npcBunniesCore::NpcAnim::MARIO && charIndex != npcBunniesCore::NpcAnim::DEVAN)
						{
							draw.anim = animIndex+1; draw.spriteMode = SPRITE::NORMAL;
							npcBunniesAnimations::DrawCharacterOnCanvas(canvas, draw);
						}
					}
				}
				else if(portraitLine.isNpc == 1)
				{
					DrawNpc = false;
					animIndex = npcBunniesAnimations::charFaces.find(jjPlayers[personId].charCurr);
					draw.add(xPos: dialogueSmallFaceX, yPos: dialogueSmallFaceY, animSet: iconAnimSet, anim: animIndex, frame: jjGameTicks/12,
						sizeX: 1, sizeY: 1, spriteMode: SPRITE::PLAYER, param: personId, grav: 0);
					npcBunniesAnimations::DrawCharacterOnCanvas(canvas, draw);
				}
			}
			//TO DO: change this depending on who is talking.

			jjTEXTAPPEARANCE ConversationAppearance(STRING::NORMAL);
			string Line = npc.dialogueOverrideLine != "" ? npc.dialogueOverrideLine : npc.dialogueSets[npc.convIndex].dialogue[npc.dialogueIndex].text;
			npcBunniesDialogue::DialogueSet@ currentDialogueSet = npc.dialogueSets[npc.convIndex];
			int textLeft = dialogueBoxX + 30;
			int textWidth = dialogueBoxW - 210 + dialogueTextExtraWidthLeft;
			if(textWidth < 160) textWidth = dialogueBoxW - 60;
			int textY = dialogueBoxY + 30;
			int textMaxY = dialogueBoxY + dialogueBoxH - 58;
			if(currentDialogueSet.choicesAnswerMethod == npcBunniesCore::DialogueAction::Quest && (currentDialogueSet.quests.length > 0 || currentDialogueSet.rewards.length > 0))
			{
				textMaxY = dialogueBoxY + dialogueBoxH - 100;
				if(npc.dialogueOverrideFinalQuestCompleted)
					canvas.drawString(textLeft, dialogueBoxY + dialogueBoxH - 74, "Final quest already completed", STRING::SMALL, STRING::PALSHIFT, 24);
				else
					npcBunniesDialogue::DrawNpcQuestSetObjectives(canvas, currentDialogueSet, textLeft, dialogueBoxY + dialogueBoxH - 88, textWidth, 50, 20);
			}
			DrawNpcDialogueScrollableText(canvas, Line, textLeft, textY, textWidth, textMaxY);

			if(npc.isLastLineInDialogueSet() && npc.dialogueSets[npc.convIndex].choicesAnswerMethod != 0)
			{
				//TO DO: display choices
				if(npc.dialogueSets[npc.convIndex].choicesAnswerMethod == 1) //choices
				{
					for(uint i=0; i<npc.dialogueSets[npc.convIndex].choices.length; i++)
					{
						string choiceText = npc.dialogueSets[npc.convIndex].choices[i].choiceText;
						canvas.drawString(0x8000 - (jjGetStringWidth(choiceText, STRING::SMALL, npcBunniesDialogue::highlightChoice == int(i) ? STRING::SPIN : STRING::NORMAL) - (jjGetStringWidth(choiceText, STRING::SMALL, npcBunniesDialogue::highlightChoice == int(i) ? STRING::SPIN : STRING::NORMAL) & ~1))/2, 350+i*20, choiceText, STRING::SMALL, npcBunniesDialogue::highlightChoice == int(i) ? STRING::SPIN : STRING::NORMAL);
						//TO DO: highlightChoice global var - which choiceText is highlighted, incremented by up/down controls.
					}
				}
				else if(npc.dialogueSets[npc.convIndex].choicesAnswerMethod == 2) //typed/text answers
				{
					string yourAnswer = "Type your answer here: ";
					canvas.drawString(0x8000 - (jjGetStringWidth(yourAnswer, STRING::SMALL, STRING::NORMAL) - (jjGetStringWidth(yourAnswer, STRING::SMALL, STRING::NORMAL) & ~1))/2, 350, yourAnswer, STRING::SMALL, STRING::NORMAL);
					canvas.drawString(0x8000 - (jjGetStringWidth(npcBunniesDialogue::answerText, STRING::SMALL, STRING::NORMAL) - (jjGetStringWidth(npcBunniesDialogue::answerText, STRING::SMALL, STRING::NORMAL) & ~1))/2, 375, npcBunniesDialogue::answerText, STRING::SMALL, STRING::NORMAL);
					//TO DO: draw a square where you display what is being typed.
				}
				//result of a choice should be in 'npc.dialogueSet[npc.convIndex].goToSetAfterChoice'
			}
		}

		//rewards were here before
	}
	npcBunniesEditorViews::NpcDesigner_Draw(play, canvas);
	return false;
}


int NpcQuestPressedKey()
{
	for(int key = 8; key <= 222; key++)
	{
		if(key == 0x01) continue;
		if(jjKey[key]) return key;
	}
	return -1;
}

void AdvanceNpcQuestDialogPage(jjPLAYER@ play, int delta)
{
	int groups = npcBunniesDialogue::CountNpcQuestGroups(play);
	if(groups <= 0) { npcBunniesCore::npcQuestDialogOpen = false; npcBunniesCore::npcQuestDialogPage = 0; return; }
	npcBunniesCore::npcQuestDialogPage += delta;
	while(npcBunniesCore::npcQuestDialogPage < 0) npcBunniesCore::npcQuestDialogPage += groups;
	while(npcBunniesCore::npcQuestDialogPage >= groups) npcBunniesCore::npcQuestDialogPage -= groups;
	npcBunniesCore::npcQuestDialogScroll = 0;
	npcBunniesDialogue::InvalidateNpcQuestDialogCache();
}

void ToggleNpcQuestDialog(jjPLAYER@ play)
{
	if(!npcBunniesCore::npcQuestDialogOpen)
	{
		npcBunniesCore::npcQuestDialogOpen = true;
		npcBunniesCore::npcQuestDialogPage = 0;
		npcBunniesCore::npcQuestDialogScroll = 0;
		npcBunniesDialogue::InvalidateNpcQuestDialogCache();
	}
	else
	{
		int groups = npcBunniesDialogue::CountNpcQuestGroups(play);
		if(groups <= 0 || npcBunniesCore::npcQuestDialogPage + 1 >= groups) npcBunniesCore::npcQuestDialogOpen = false;
		else npcBunniesCore::npcQuestDialogPage++;
		npcBunniesDialogue::InvalidateNpcQuestDialogCache();
	}
	npcBunniesCore::npcQuestDialogCaptureKey = false;
	npcBunniesCore::npcQuestDialogInputTick = jjGameTicks;
}

void ProcessNpcQuestDialogInput()
{
	if(npcBunniesEditorCore::NpcDesignerOpen || npcBunniesCore::chatIsOpen || npcBunniesMovement::convOn != -1) return;
	bool mouseDown = jjKey[0x01];
	bool clicked = mouseDown && !npcBunniesCore::npcQuestMouseWasDown;
	if(!mouseDown)
	{
		npcBunniesCore::npcQuestMouseWasDown = false;
	}
	else npcBunniesCore::npcQuestMouseWasDown = true;

	if(npcBunniesCore::npcQuestDialogCaptureKey && !npcBunniesDialogue::NpcQuestHotkeyEnabled())
		npcBunniesCore::npcQuestDialogCaptureKey = false;

	if(npcBunniesCore::npcQuestDialogCaptureKey)
	{
		int key = NpcQuestPressedKey();
		if(key > 0 && key != 0x1B && jjGameTicks > npcBunniesCore::npcQuestDialogInputTick + 10)
		{
			npcBunniesCore::NPC_QUEST_DIALOG_KEY = key;
			npcBunniesCore::npcQuestDialogCaptureKey = false;
			npcBunniesCore::npcQuestDialogInputTick = jjGameTicks;
			npcBunniesCore::SaveNpcBunniesPreferences();
		}
		else if(jjKey[0x1B] && jjGameTicks > npcBunniesCore::npcQuestDialogInputTick + 10)
		{
			npcBunniesCore::npcQuestDialogCaptureKey = false;
			npcBunniesCore::npcQuestDialogInputTick = jjGameTicks;
		}
		return;
	}

	bool questKeyPressed = npcBunniesDialogue::NpcQuestHotkeyEnabled() && jjKey[npcBunniesCore::NPC_QUEST_DIALOG_KEY] && jjGameTicks > npcBunniesCore::npcQuestDialogInputTick + 20;
	if(!questKeyPressed && !clicked && !npcBunniesCore::npcQuestDialogOpen) return;

	jjPLAYER@ questPlayer = npcBunniesDialogue::NpcQuestLocalPlayer();
	if(questKeyPressed)
	{
		ToggleNpcQuestDialog(questPlayer);
		return;
	}
	if(clicked && npcBunniesDialogue::NpcQuestHotkeyEnabled())
	{
		int hudX = npcBunniesCore::HUD_COUNTS_X;
		int hudY = npcBunniesCore::HUD_COUNTS_Y + 32;
		int hudW = npcBunniesDialogue::NpcQuestHudWidth();
		int tx = jjMouseX;
		int ty = jjMouseY + 8;
		if(tx >= hudX && tx <= hudX + hudW && ty >= hudY && ty <= hudY + 14)
		{
			ToggleNpcQuestDialog(questPlayer);
			return;
		}
	}
	if(!npcBunniesCore::npcQuestDialogOpen) return;
	int w = 315;
	int textW = npcBunniesCore::npcQuestDialogShowInfo ? w - 20 - 62 : w - 20;
	if(textW < 20) textW = w - 20;
	int visibleHeight = npcBunniesCore::npcQuestDialogShowInfo ? 69 : 14;
	npcBunniesDialogue::EnsureNpcQuestDialogCache(questPlayer, npcBunniesCore::npcQuestDialogPage, npcBunniesCore::npcQuestDialogShowInfo, textW, visibleHeight);
	if(npcBunniesDialogue::cachedNpcQuestDialogGroups <= 0) npcBunniesCore::npcQuestDialogPage = 0;
	else if(npcBunniesCore::npcQuestDialogPage >= npcBunniesDialogue::cachedNpcQuestDialogGroups)
	{
		npcBunniesCore::npcQuestDialogPage = npcBunniesDialogue::cachedNpcQuestDialogGroups - 1;
		npcBunniesDialogue::EnsureNpcQuestDialogCache(questPlayer, npcBunniesCore::npcQuestDialogPage, npcBunniesCore::npcQuestDialogShowInfo, textW, visibleHeight, true);
	}
	else if(npcBunniesCore::npcQuestDialogPage < 0)
	{
		npcBunniesCore::npcQuestDialogPage = 0;
		npcBunniesDialogue::EnsureNpcQuestDialogCache(questPlayer, npcBunniesCore::npcQuestDialogPage, npcBunniesCore::npcQuestDialogShowInfo, textW, visibleHeight, true);
	}
	int h = npcBunniesCore::npcQuestDialogShowInfo ? 180 : 58 + npcBunniesDialogue::cachedNpcQuestDialogBasicLines * 18;
	if(h < 110) h = 110;
	npcBunniesDialogue::EnsureNpcQuestDialogPosition(w, h);
	npcBunniesCore::npcQuestDialogWindow.preferencesFile = "npcBunniesDialogueBox.asdat";
	DbBegin(@npcBunniesCore::npcQuestDialogWindow, "npcQuestTracker", "Active quests", npcBunniesCore::npcQuestDialogX, npcBunniesCore::npcQuestDialogY, w, h);
	npcBunniesCore::npcQuestDialogWindow.closeButton = true;
	npcBunniesCore::npcQuestDialogWindow.draggable = true;
	npcBunniesCore::npcQuestDialogWindow.titleBar = true;
	npcBunniesCore::npcQuestDialogWindow.pad = 10;
	npcBunniesCore::npcQuestDialogX = npcBunniesCore::npcQuestDialogWindow.x;
	npcBunniesCore::npcQuestDialogY = npcBunniesCore::npcQuestDialogWindow.y;
	DbAddControl(@npcBunniesCore::npcQuestDialogWindow, npcBunniesCore::npcQuestDialogX + w - 24, npcBunniesCore::npcQuestDialogY + 8, 18, 15, DB_ACTION_CLOSE, 0, 0, "X");
	int textX = npcBunniesCore::npcQuestDialogX + 10;
	int textY = npcBunniesCore::npcQuestDialogY + 33;
	int controlsY = npcBunniesCore::npcQuestDialogY + h - 22;
	if(npcBunniesCore::npcQuestDialogShowInfo)
	{
		int objectivesY = controlsY - 50;
		int textMaxY = objectivesY - 6;
		if(npcBunniesCore::npcQuestDialogScroll < 0) npcBunniesCore::npcQuestDialogScroll = 0;
		if(npcBunniesCore::npcQuestDialogScroll > npcBunniesDialogue::cachedNpcQuestDialogMaxScroll) npcBunniesCore::npcQuestDialogScroll = npcBunniesDialogue::cachedNpcQuestDialogMaxScroll;
		if(npcBunniesDialogue::cachedNpcQuestDialogMaxScroll > 0)
		{
			if(npcBunniesCore::npcQuestDialogScroll > 0) DbAddControl(@npcBunniesCore::npcQuestDialogWindow, textX + w - 20 - 46, textY, 42, 14, npcBunniesCore::DB_NPC_QUEST_SCROLL_UP, 0, -1, "UP");
			if(npcBunniesCore::npcQuestDialogScroll < npcBunniesDialogue::cachedNpcQuestDialogMaxScroll) DbAddControl(@npcBunniesCore::npcQuestDialogWindow, textX + w - 20 - 58, textMaxY - 12, 54, 14, npcBunniesCore::DB_NPC_QUEST_SCROLL_DOWN, 0, 1, "DOWN");
		}
	}
	else npcBunniesCore::npcQuestDialogScroll = 0;
	if(npcBunniesDialogue::NpcQuestHotkeyEnabled())
		DbAddControl(@npcBunniesCore::npcQuestDialogWindow, npcBunniesCore::npcQuestDialogX + 10, controlsY, 90, 14, npcBunniesCore::DB_NPC_QUEST_KEY, 0, 0, "Key");
	DbAddControl(@npcBunniesCore::npcQuestDialogWindow, npcBunniesCore::npcQuestDialogX + 112, controlsY - 1, 16, 14, npcBunniesCore::DB_NPC_QUEST_PREV, 0, -1, "<");
	DbAddControl(@npcBunniesCore::npcQuestDialogWindow, npcBunniesCore::npcQuestDialogX + 136, controlsY - 1, 16, 14, npcBunniesCore::DB_NPC_QUEST_NEXT, 0, 1, ">");
	DbAddControl(@npcBunniesCore::npcQuestDialogWindow, npcBunniesCore::npcQuestDialogX + 164, controlsY, 130, 14, npcBunniesCore::DB_NPC_QUEST_INFO, 0, 0, "Show info");
	if(jjKey[0x1B] && jjGameTicks > npcBunniesCore::npcQuestDialogInputTick + 20)
	{
		npcBunniesCore::npcQuestDialogOpen = false;
		npcBunniesCore::npcQuestDialogInputTick = jjGameTicks;
		return;
	}
	DbResult result;
	if(DbProcessMouse(@npcBunniesCore::npcQuestDialogWindow, @result) || DbProcessKeyboard(@npcBunniesCore::npcQuestDialogWindow, questPlayer, @result, 10))
	{
		npcBunniesCore::npcQuestDialogX = npcBunniesCore::npcQuestDialogWindow.x;
		npcBunniesCore::npcQuestDialogY = npcBunniesCore::npcQuestDialogWindow.y;
		if(result.positionChanged) npcBunniesCore::SaveNpcBunniesPreferences();
		if(result.closeRequested)
		{
			npcBunniesCore::npcQuestDialogOpen = false;
			npcBunniesCore::npcQuestDialogInputTick = jjGameTicks;
		}
		else if(result.action == npcBunniesCore::DB_NPC_QUEST_PREV || result.action == npcBunniesCore::DB_NPC_QUEST_NEXT)
		{
			AdvanceNpcQuestDialogPage(questPlayer, result.b);
			npcBunniesCore::npcQuestDialogInputTick = jjGameTicks;
		}
		else if(DbIsScrollResult(result, npcBunniesCore::DB_NPC_QUEST_SCROLL_UP, npcBunniesCore::DB_NPC_QUEST_SCROLL_DOWN))
		{
			npcBunniesCore::npcQuestDialogScroll = DbHandleScrollResult(result, npcBunniesCore::npcQuestDialogScroll, npcBunniesCore::DB_NPC_QUEST_SCROLL_UP, npcBunniesCore::DB_NPC_QUEST_SCROLL_DOWN);
			npcBunniesCore::npcQuestDialogInputTick = jjGameTicks;
		}
		else if(result.action == npcBunniesCore::DB_NPC_QUEST_INFO)
		{
			npcBunniesCore::npcQuestDialogShowInfo = !npcBunniesCore::npcQuestDialogShowInfo;
			npcBunniesCore::npcQuestDialogScroll = 0;
			npcBunniesDialogue::InvalidateNpcQuestDialogCache();
			npcBunniesCore::npcQuestDialogInputTick = jjGameTicks;
		}
		else if(result.action == npcBunniesCore::DB_NPC_QUEST_KEY && npcBunniesDialogue::NpcQuestHotkeyEnabled())
		{
			npcBunniesCore::npcQuestDialogCaptureKey = true;
			npcBunniesCore::npcQuestDialogInputTick = jjGameTicks;
		}
	}
}

void onPlayerInput(jjPLAYER@ play)
{
	npcBunniesEditorCore::NpcDesigner_OnPlayerInput(play);
	if(npcBunniesEditorCore::NpcDesignerOpen && npcBunniesCore::LocalPlayerHasNpcBunniesAdminAccess()) { jjKeyChat = 5; return; }
	//TODO: this is basically a method that changes the jjKeyChat so that you can use it when answering questions
	//so maybe move this in a separate method (?)
	if(npcBunniesMovement::convOn != -1)
	{
		if(npcBunniesCore::npcBunnies[npcBunniesMovement::convOn].isLastLineInDialogueSet() && npcBunniesCore::npcBunnies[npcBunniesMovement::convOn].dialogueSets[npcBunniesCore::npcBunnies[npcBunniesMovement::convOn].convIndex].choicesAnswerMethod == 2)
		{
			//play.keyLeft = play.keyRight = play.keyRun = play.keyFire = play.keySelect = play.keyJump = false;
			jjKeyChat = 5;
		}
		else
		{
			jjKeyChat = 84;
		}
	}
	else
	{
		jjKeyChat = 84;
	}
	if(jjGameTicks <= npcBunniesMovement::keyTicks[13] + 20) play.keySelect = false;
}


bool onLocalChat(string &in text, CHAT::Type type)
{
	if(npcBunniesAccessories::NpcAccessories::OnLocalChat(text)) return true;
	if(jjRegexMatch(text, "^!quests$", true)) { npcBunniesCore::npcQuestDialogOpen = true; npcBunniesCore::npcQuestDialogPage = 0; npcBunniesCore::npcQuestDialogCaptureKey = false; npcBunniesDialogue::InvalidateNpcQuestDialogCache(); return true; }
	if(jjRegexMatch(text, "^!npcSync\\s+(ON|OFF)$", true))
	{
		array<string> parts = text.split(" ");
		bool enabled = npcBunniesCore::ENABLE_NPC_SYNC;
		string value = "";
		for(uint i = 1; i < parts.length; i++)
		{
			if(parts[i] != "")
			{
				value = parts[i];
				break;
			}
		}
		if(value == "" || !npcBunniesCore::ParseNpcSyncSettingValue(value, enabled)) return true;
		if(!LocalPlayerHasAnyAdminAccess())
		{
			jjAlert("NpcBunnies: only admins can change NPC sync.", false);
			return true;
		}
		if(jjIsServer) SaveNpcSyncSettingCommand(enabled);
		else
		{
			jjSTREAM packet;
			packet.push("npcSyncSettingSet " + (enabled ? "1" : "0"));
			jjSendPacket(packet);
			jjAlert("NpcBunnies: requested NPC sync " + npcBunniesCore::NpcSyncSettingText(enabled) + ". It takes effect after level cycle.", false);
		}
		return true;
	}
	if(npcBunniesCore::LocalPlayerHasNpcBunniesAdminAccess() && jjRegexMatch(text, "^!respawnMonsters$", true))
	{
		if(jjIsServer)
		{
			int respawned = npcBunniesQuests::RespawnNpcFightingTargets(true);
			jjAlert("NpcBunniesAdmin: respawned " + respawned + " monster/boss objects.", false);
		}
		else
		{
			npcBunniesQuests::RequestRespawnNpcFightingTargets();
			jjAlert("NpcBunniesAdmin: requested monster respawn from the server.", false);
		}
		return true;
	}
	return npcBunniesEditorCore::NpcDesigner_OnLocalChat(text, type);
}


void exitConv()
{
	npcBunniesMovement::convOn = -1;
}

void requestNpcBunniesAdminAccess()
{
	if(jjIsServer || npcBunniesCore::npcBunniesAdminAccessGranted || npcBunniesCore::npcBunniesAdminAccessRequestTick + 70 > jjGameTicks) return;
	npcBunniesCore::npcBunniesAdminAccessRequestTick = jjGameTicks;
	jjSTREAM packet;
	packet.push("npcAdminAccess");
	jjSendPacket(packet);
}

int ClientIdToPlayerID(int clientID)
{
	for(int i = 0; i < 32; i++)
	{
		if((jjPlayers[i].isActive || jjPlayers[i].isInGame) && jjPlayers[i].clientID == clientID) return i;
	}
	return -1;
}

bool PlayerHasAnyAdminAccess(jjPLAYER@ play)
{
	if(play is null) return false;
	return play.isAdmin || play.hasPrivilege("admin");
}

bool LocalPlayerHasAnyAdminAccess()
{
	if(jjIsServer || jjIsAdmin) return true;
	for(int i = 0; i < jjLocalPlayerCount; i++)
	{
		if(PlayerHasAnyAdminAccess(jjLocalPlayers[i])) return true;
	}
	return false;
}

bool ClientHasAnyAdminAccess(int clientID)
{
	if(clientID == 0) return jjIsServer;
	int playerID = ClientIdToPlayerID(clientID);
	return playerID >= 0 && playerID < 32 && PlayerHasAnyAdminAccess(jjPlayers[playerID]);
}

void requestNpcSyncSetting()
{
	jjSTREAM packet;
	packet.push("npcSyncSettingRequest");
	jjSendPacket(packet);
}

void sendNpcSyncSetting(int clientID)
{
	if(!jjIsServer) return;
	jjSTREAM packet;
	packet.push("npcSyncSetting " + (npcBunniesCore::ENABLE_NPC_SYNC ? "1" : "0"));
	if(clientID == 0) jjSendPacket(packet);
	else jjSendPacket(packet, clientID);
}

void sendNpcSyncSettingSaved(int clientID, bool enabled, bool allowed)
{
	if(!jjIsServer || clientID <= 0) return;
	jjSTREAM packet;
	packet.push("npcSyncSettingSaved " + (allowed ? "1" : "0") + " " + (enabled ? "1" : "0"));
	jjSendPacket(packet, clientID);
}

void SaveNpcSyncSettingCommand(bool enabled)
{
	if(!jjIsServer) return;
	npcBunniesCore::SaveNpcSyncSetting(enabled);
	jjAlert("NpcBunnies: NPC sync " + npcBunniesCore::NpcSyncSettingText(enabled) + " saved. It takes effect after level cycle.", false);
}

void sendNpcBunniesAdminAccess(int clientID)
{
	if(!jjIsServer || clientID <= 0) return;
	int playerID = int(npcBunniesMovement::clientToID(clientID));
	bool allowed = playerID >= 0 && playerID < 32 && npcBunniesCore::HasNpcBunniesAdminPrivilege(jjPlayers[playerID]);
	jjSTREAM packet;
	packet.push("npcAdminAccess " + (allowed ? "1" : "0"));
	jjSendPacket(packet, clientID);
}
void requestNpcServerSync()
{
	if(!npcBunniesCore::ENABLE_NPC_SYNC) return;
	jjSTREAM npcPacket;
	npcPacket.push("npcAll");
	jjSendPacket(npcPacket);
}

void applyNpcSync(int id, int playerFollowing, int followingTrainIndex, float xPos, float yPos, int direction, int hatFrame = -9999, int scarfFrame = -9999, int glassesFrame = -9999)
{
	if(id < 0 || id >= int(npcBunniesCore::npcBunnies.length)) return;
	npcBunniesMovement::EnsureNpcRuntimeArrays();
	npcBunniesCore::npcBunnies[id].playerFollowing = playerFollowing;
	npcBunniesCore::npcBunnies[id].followingTrainIndex = followingTrainIndex;
	if(hatFrame != -9999) npcBunniesCore::npcBunnies[id].hatFrame = hatFrame;
	if(scarfFrame != -9999) npcBunniesCore::npcBunnies[id].scarfFrame = scarfFrame;
	if(glassesFrame != -9999) npcBunniesCore::npcBunnies[id].glassesFrame = glassesFrame;
	jjOBJ@ obj = jjObjects[npcBunniesCore::npcBunnies[id].objectID];
	obj.xPos = xPos;
	obj.yPos = yPos;
	obj.direction = direction;
	npcBunniesMovement::previousNpcX[id] = xPos;
	npcBunniesMovement::previousNpcY[id] = yPos;
}

void sendNpcSync(int id, int playerFollowing, int followingTrainIndex, int clientID = 0)
{
	if(!npcBunniesCore::ENABLE_NPC_SYNC) return;
	if(id < 0 || id >= int(npcBunniesCore::npcBunnies.length)) return;
	jjOBJ@ obj = jjObjects[npcBunniesCore::npcBunnies[id].objectID];
	jjSTREAM npcPacket;
	npcPacket.push("npcSync " + id + " " + playerFollowing + " " + followingTrainIndex + " " + obj.xPos + " " + obj.yPos + " " + obj.direction + " " + npcBunniesCore::npcBunnies[id].hatFrame + " " + npcBunniesCore::npcBunnies[id].scarfFrame + " " + npcBunniesCore::npcBunnies[id].glassesFrame);
	if(clientID == 0) jjSendPacket(npcPacket);
	else jjSendPacket(npcPacket, clientID);
	if(jjIsServer && clientID == 0)
	{
		npcBunniesMovement::EnsureNpcRuntimeArrays();
		if(id < int(npcBunniesMovement::lastNpcSyncedX.length))
		{
			npcBunniesMovement::lastNpcSyncedX[id] = obj.xPos;
			npcBunniesMovement::lastNpcSyncedY[id] = obj.yPos;
			npcBunniesMovement::lastNpcPositionSyncTick[id] = jjGameTicks;
		}
	}
}

void sendAllNpcSync(int clientID)
{
	if(!npcBunniesCore::ENABLE_NPC_SYNC) return;
	if(clientID <= 0) return;
	for(uint i = 0; i < npcBunniesCore::npcBunnies.length; i++)
	{
		sendNpcSync(int(i), npcBunniesCore::npcBunnies[i].playerFollowing, npcBunniesCore::npcBunnies[i].followingTrainIndex, clientID);
		npcBunniesFighting::SendNpcFightingSync(int(i), clientID);
	}
}

void MaybeSendNpcPositionSync(npcBunniesCore::npcBunny@ npc, jjOBJ@ obj, float oldX, float oldY)
{
	if(!npcBunniesCore::ENABLE_NPC_SYNC || !npcBunniesCore::ENABLE_NPC_POSITION_SYNC) return;
	if(!jjIsServer || npc is null) return;
	if(npc.id < 0 || npc.id >= int(npcBunniesMovement::lastNpcPositionSyncTick.length)) return;
	if(npc.playerFollowing < 0 && !npc.guideActive && npc.tpAnim < 0) return;
	if(oldX == obj.xPos && oldY == obj.yPos) return;
	if(npcBunniesMovement::lastNpcPositionSyncTick[npc.id] + npcBunniesCore::NPC_POSITION_SYNC_INTERVAL > jjGameTicks) return;
	float dx = abs(obj.xPos - npcBunniesMovement::lastNpcSyncedX[npc.id]);
	float dy = abs(obj.yPos - npcBunniesMovement::lastNpcSyncedY[npc.id]);
	if(dx < npcBunniesCore::NPC_POSITION_SYNC_MIN_DISTANCE && dy < npcBunniesCore::NPC_POSITION_SYNC_MIN_DISTANCE) return;
	npcBunniesMovement::lastNpcPositionSyncTick[npc.id] = jjGameTicks;
	sendNpcSync(npc.id, npc.playerFollowing, npc.followingTrainIndex, 0);
}

void onReceive(jjSTREAM &in packet, int fromClientID)
{
	string command;
	packet.pop(command);
	array<string> string_parts = command.split(" ");
	if(command == "npcEditorBegin")
	{
		npcBunniesCore::npcDesignerSyncText = "";
		npcBunniesCore::npcDesignerSyncAllowed = !jjIsServer || npcBunniesCore::HasNpcBunniesAdminPrivilege(jjPlayers[int(npcBunniesMovement::clientToID(fromClientID))]);
		return;
	}
	if(command == "npcEditorChunk")
	{
		string chunk;
		packet.pop(chunk);
		if(npcBunniesCore::npcDesignerSyncAllowed) npcBunniesCore::npcDesignerSyncText += npcBunniesEditorSerialization::NdePacketDecode(chunk);
		return;
	}
	if(command == "npcEditorEnd")
	{
		if(npcBunniesCore::npcDesignerSyncAllowed && npcBunniesCore::npcDesignerSyncText != "")
		{
			npcBunniesEditorSerialization::NdeApplyNpcDesignerState(npcBunniesCore::npcDesignerSyncText, jjIsServer);
			if(jjIsServer) npcBunniesEditorSerialization::NdeBroadcastNpcDesignerState(npcBunniesCore::npcDesignerSyncText, -fromClientID);
		}
		npcBunniesCore::npcDesignerSyncText = "";
		npcBunniesCore::npcDesignerSyncAllowed = false;
		return;
	}
	if(command == "npcAdminAccess" && jjIsServer)
	{
		sendNpcBunniesAdminAccess(fromClientID);
		return;
	}
	if(string_parts.length >= 2 && string_parts[0] == "npcAdminAccess" && !jjIsServer)
	{
		npcBunniesCore::npcBunniesAdminAccessGranted = parseInt(string_parts[1]) != 0;
		return;
	}
	if(command == "npcSyncSettingRequest" && jjIsServer)
	{
		sendNpcSyncSetting(fromClientID);
		return;
	}
	if(string_parts.length >= 2 && string_parts[0] == "npcSyncSetting")
	{
		npcBunniesCore::ENABLE_NPC_SYNC = parseInt(string_parts[1]) != 0;
		receivedNpcSyncSetting = true;
		return;
	}
	if(string_parts.length >= 2 && string_parts[0] == "npcSyncSettingSet" && jjIsServer)
	{
		bool enabled = parseInt(string_parts[1]) != 0;
		bool allowed = ClientHasAnyAdminAccess(fromClientID);
		if(allowed) npcBunniesCore::SaveNpcSyncSetting(enabled);
		sendNpcSyncSettingSaved(fromClientID, enabled, allowed);
		return;
	}
	if(string_parts.length >= 3 && string_parts[0] == "npcSyncSettingSaved" && !jjIsServer)
	{
		bool allowed = parseInt(string_parts[1]) != 0;
		bool enabled = parseInt(string_parts[2]) != 0;
		if(allowed) jjAlert("NpcBunnies: NPC sync " + npcBunniesCore::NpcSyncSettingText(enabled) + " saved. It takes effect after level cycle.", false);
		else jjAlert("NpcBunnies: server denied NPC sync setting change.", false);
		return;
	}
	if(!npcBunniesCore::ENABLE_NPC_SYNC && (command == "npcAll" || (string_parts.length >= 1 && (string_parts[0] == "npcSync" || string_parts[0] == "npcFight")))) return;
	if(command == "npcAll" && jjIsServer)
	{
		sendAllNpcSync(fromClientID);
		return;
	}
	if(command == "npcRespawnMonsters" && jjIsServer)
	{
		int senderID = int(npcBunniesMovement::clientToID(fromClientID));
		if(fromClientID == 0 || (senderID >= 0 && senderID < 32 && npcBunniesCore::HasNpcBunniesAdminPrivilege(jjPlayers[senderID]))) npcBunniesQuests::RespawnNpcFightingTargets(true);
		return;
	}
	if(string_parts.length >= 4 && string_parts[0] == "npcRespawnMonsterAt" && !jjIsServer)
	{
		int eventID = parseInt(string_parts[1]);
		int xTile = parseInt(string_parts[2]);
		int yTile = parseInt(string_parts[3]);
		npcBunniesQuests::RespawnNpcFightingTargetAt(eventID, xTile, yTile);
		return;
	}
	if(string_parts.length >= 2 && string_parts[0] == "npcReturn" && jjIsServer)
	{
		int id = parseInt(string_parts[1]);
		npcBunniesDialogue::ReturnNpcToOrigin(id);
		return;
	}
	if(string_parts.length >= 3 && string_parts[0] == "npcFight")
	{
		int id = parseInt(string_parts[1]);
		bool enabled = parseInt(string_parts[2]) != 0;
		if(jjIsServer && fromClientID != 0)
		{
			int senderID = int(npcBunniesMovement::clientToID(fromClientID));
			if(id >= 0 && id < int(npcBunniesCore::npcBunnies.length) && npcBunniesCore::npcBunnies[id].playerFollowing == senderID) npcBunniesFighting::ApplyNpcFightingSync(id, enabled);
			npcBunniesFighting::SendNpcFightingSync(id, 0);
		}
		else npcBunniesFighting::ApplyNpcFightingSync(id, enabled);
		return;
	}
	if(string_parts.length < 7 || string_parts[0] != "npcSync") return;
	int id = parseInt(string_parts[1]);
	int playerFollowing = parseInt(string_parts[2]);
	int followingTrainIndex = parseInt(string_parts[3]);
	float xPos = parseFloat(string_parts[4]);
	float yPos = parseFloat(string_parts[5]);
	int direction = parseInt(string_parts[6]);
	int hatFrame = string_parts.length >= 10 ? parseInt(string_parts[7]) : -9999;
	int scarfFrame = string_parts.length >= 10 ? parseInt(string_parts[8]) : -9999;
	int glassesFrame = string_parts.length >= 10 ? parseInt(string_parts[9]) : -9999;
	if(id < 0 || id >= int(npcBunniesCore::npcBunnies.length)) return;
	if(jjIsServer && fromClientID != 0)
	{
		int senderID = int(npcBunniesMovement::clientToID(fromClientID));
		if(playerFollowing == -1)
		{
			if(npcBunniesCore::npcBunnies[id].playerFollowing == senderID)
			{
				npcBunniesMovement::recalculateFollowingTrainIndexes(npcBunniesCore::npcBunnies[id].followingTrainIndex, senderID);
				npcBunniesCore::npcBunnies[id].playerFollowing = -1;
				npcBunniesCore::npcBunnies[id].followingTrainIndex = -1;
			}
		}
		else if(playerFollowing == senderID && npcBunniesCore::npcBunnies[id].playerFollowing == -1)
		{
			int nextIndex = npcBunniesMovement::getFollowingTrainIndex(senderID);
			if(npcBunniesCore::NPC_FOLLOW_LIMIT == -1 || nextIndex <= npcBunniesCore::NPC_FOLLOW_LIMIT)
			{
				npcBunniesCore::npcBunnies[id].playerFollowing = senderID;
				npcBunniesCore::npcBunnies[id].followingTrainIndex = nextIndex;
			}
		}
		sendNpcSync(id, npcBunniesCore::npcBunnies[id].playerFollowing, npcBunniesCore::npcBunnies[id].followingTrainIndex, 0);
		return;
	}
	applyNpcSync(id, playerFollowing, followingTrainIndex, xPos, yPos, direction, hatFrame, scarfFrame, glassesFrame);
}


void onObjectHit(jjOBJ@ obj, jjOBJ@ bullet, jjPLAYER@ player, int force)
{
	if(!npcBunniesCore::ENABLE_NPC_MONSTER_QUESTS) return;
	if(player is null || obj is null) return;
	if(!npcBunniesQuests::IsTrackedDefeatEventID(obj.eventID)) return;
	npcBunniesQuests::EnsureTrackedDefeatObjectArrays();
	int id = obj.objectID;
	if(id >= 0 && id < int(npcBunniesQuests::trackedDefeatObjectLastHitPlayer.length))
	{
		npcBunniesQuests::trackedDefeatObjectWasActive[id] = true;
		npcBunniesQuests::trackedDefeatObjectEventID[id] = obj.eventID;
		npcBunniesQuests::trackedDefeatObjectLastHitPlayer[id] = player.nameUnformatted;
		if(force >= obj.energy && !npcBunniesQuests::trackedDefeatObjectCounted[id])
		{
			npcBunniesQuests::IncrementDefeatedObjectCountForName(player.nameUnformatted, obj.eventID);
			npcBunniesQuests::IncrementActiveQuestDefeatProgressForName(player.nameUnformatted, obj.eventID);
			npcBunniesQuests::trackedDefeatObjectCounted[id] = true;
		}
	}
}

}