Downloads containing npcBunniesParser.asc

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

File preview

namespace npcBunniesParser
{
void LoadNpcDialogueFile()
{
	string baseName = jjLevelFileName;
	int dot = int(baseName.findLast("."));
	if(dot > 0) baseName = baseName.substr(0, dot);
	if(LoadNpcDialogueFileByName(baseName + "_npc.asdat")) return;
	LoadNpcDialogueFileByName(baseName + "_npc.asdat");
}

bool LoadNpcDialogueFileByName(const string &in filename)
{
	jjSTREAM file(filename);
	if(file.isEmpty()) return false;
	string text = "", line = "";
	while(file.getLine(line)) text += line + "\n";
	array<string> tokens = TokenizeNpcJson(StripNpcJsonComments(text));
	array<int> pos = {0};
	ParseNpcJsonRoot(tokens, pos);
	return npcBunniesCore::npcBunnies.length > 0;
}

string StripNpcJsonComments(const string &in text)
{
	string jsonOut = "";
	bool inString = false, lineComment = false, blockComment = false;
	for(uint i = 0; i < text.length; i++)
	{
		uint8 c = text[i], n = (i + 1 < text.length) ? text[i + 1] : 0;
		if(lineComment) { if(c == 10 || c == 13) { lineComment = false; jsonOut += "\n"; } continue; }
		if(blockComment) { if(c == 42 && n == 47) { blockComment = false; i++; } continue; }
		if(!inString && c == 47 && n == 47) { lineComment = true; i++; continue; }
		if(!inString && c == 47 && n == 42) { blockComment = true; i++; continue; }
		if(c == 34 && (i == 0 || text[i - 1] != 92)) inString = !inString;
		jsonOut += text.substr(i, 1);
	}
	return jsonOut;
}

array<string> TokenizeNpcJson(const string &in text)
{
	array<string> tokens;
	for(uint i = 0; i < text.length; i++)
	{
		uint8 c = text[i];
		if(c <= 32) continue;
		if(c == 123 || c == 125 || c == 91 || c == 93 || c == 58 || c == 44) { tokens.insertLast(text.substr(i, 1)); continue; }
		if(c == 34)
		{
			string s = ""; i++;
			while(i < text.length)
			{
				uint8 sc = text[i];
				if(sc == 34 && (i == 0 || text[i - 1] != 92)) break;
				if(sc == 92 && i + 1 < text.length) { i++; if(text[i] == 110) s += "\n"; else s += text.substr(i, 1); }
				else s += text.substr(i, 1);
				i++;
			}
			tokens.insertLast(s);
			continue;
		}
		string bare = "";
		while(i < text.length)
		{
			uint8 bc = text[i];
			if(bc == 58 && i + 1 < text.length && text[i + 1] == 58) { bare += "::"; i += 2; continue; }
			if(bc <= 32 || bc == 123 || bc == 125 || bc == 91 || bc == 93 || bc == 58 || bc == 44) { i--; break; }
			bare += text.substr(i, 1); i++;
		}
		if(bare != "") tokens.insertLast(bare);
	}
	return tokens;
}

void ParseNpcJsonRoot(array<string>@ tokens, array<int>@ pos)
{
	if(pos[0] >= int(tokens.length)) return;
	if(tokens[pos[0]] == "[") { ParseNpcArray(tokens, pos); return; }
	if(tokens[pos[0]] != "{") return;
	pos[0]++;
	while(pos[0] < int(tokens.length) && tokens[pos[0]] != "}")
	{
		string key = tokens[pos[0]++]; if(pos[0] < int(tokens.length) && tokens[pos[0]] == ":") pos[0]++;
		if(jjRegexMatch(key, "NPCS", true)) ParseNpcArray(tokens, pos);
		else if(jjRegexMatch(key, "Hud", true) || jjRegexMatch(key, "HUD", true) || jjRegexMatch(key, "CountsHud", true) || jjRegexMatch(key, "CountsHUD", true)) ParseNpcHudSettings(tokens, pos);
		else if(jjRegexMatch(key, "LoadBigAnimations", true) || jjRegexMatch(key, "BigAnimations", true) || jjRegexMatch(key, "MenuAnims", true)) npcBunniesCore::LOAD_BIG_NPC_ANIMATIONS = ReadNpcJsonBool(tokens, pos);
		else if(jjRegexMatch(key, "LoadAllMonsterAnimations", true) || jjRegexMatch(key, "LoadAllMonsterBossAnimations", true) || jjRegexMatch(key, "AllMonsterBossAnimations", true)) npcBunniesCore::LOAD_ALL_MONSTER_ANIMATIONS = ReadNpcJsonBool(tokens, pos);
		else if(jjRegexMatch(key, "AdditionalMonsterAnimations", true) || jjRegexMatch(key, "AdditionalMonsterBossAnimations", true) || jjRegexMatch(key, "LoadAdditionalMonsters", true)) ParseNpcAdditionalMonsterAnimations(tokens, pos);
		else SkipNpcJsonValue(tokens, pos);
		if(pos[0] < int(tokens.length) && tokens[pos[0]] == ",") pos[0]++;
	}
}

void ParseNpcAdditionalMonsterAnimations(array<string>@ tokens, array<int>@ pos)
{
	npcBunniesCore::NPC_ADDITIONAL_MONSTER_ANIMATIONS.resize(0);
	if(pos[0] >= int(tokens.length) || tokens[pos[0]] != "[") { SkipNpcJsonValue(tokens, pos); return; }
	pos[0]++;
	while(pos[0] < int(tokens.length) && tokens[pos[0]] != "]")
	{
		int type = npcBunniesAnimations::getTypeFromTypeName(ReadNpcJsonString(tokens, pos));
		if(npcBunniesAnimations::IsNpcSimpleAnimType(type) && npcBunniesCore::NPC_ADDITIONAL_MONSTER_ANIMATIONS.find(type) < 0) npcBunniesCore::NPC_ADDITIONAL_MONSTER_ANIMATIONS.insertLast(type);
		if(pos[0] < int(tokens.length) && tokens[pos[0]] == ",") pos[0]++;
	}
	if(pos[0] < int(tokens.length) && tokens[pos[0]] == "]") pos[0]++;
}
void ParseNpcHudSettings(array<string>@ tokens, array<int>@ pos)
{
	if(pos[0] >= int(tokens.length) || tokens[pos[0]] != "{") { SkipNpcJsonValue(tokens, pos); return; }
	pos[0]++;
	while(pos[0] < int(tokens.length) && tokens[pos[0]] != "}")
	{
		string key = tokens[pos[0]++]; if(pos[0] < int(tokens.length) && tokens[pos[0]] == ":") pos[0]++;
		if(jjRegexMatch(key, "On", true) || jjRegexMatch(key, "Enabled", true) || jjRegexMatch(key, "Show", true) || jjRegexMatch(key, "ShowCounts", true)) npcBunniesCore::HUD_COUNTS_ON = ReadNpcJsonBool(tokens, pos);
		else if(jjRegexMatch(key, "X", true) || jjRegexMatch(key, "PosX", true) || jjRegexMatch(key, "OffsetX", true)) npcBunniesCore::HUD_COUNTS_X = ReadNpcJsonInt(tokens, pos);
		else if(jjRegexMatch(key, "Y", true) || jjRegexMatch(key, "PosY", true) || jjRegexMatch(key, "OffsetY", true)) npcBunniesCore::HUD_COUNTS_Y = ReadNpcJsonInt(tokens, pos);
		else SkipNpcJsonValue(tokens, pos);
		if(pos[0] < int(tokens.length) && tokens[pos[0]] == ",") pos[0]++;
	}
	if(pos[0] < int(tokens.length) && tokens[pos[0]] == "}") pos[0]++;
}
void ParseNpcArray(array<string>@ tokens, array<int>@ pos)
{
	if(pos[0] >= int(tokens.length) || tokens[pos[0]] != "[") return;
	pos[0]++;
	while(pos[0] < int(tokens.length) && tokens[pos[0]] != "]")
	{
		ParseNpcEntry(tokens, pos);
		if(pos[0] < int(tokens.length) && tokens[pos[0]] == ",") pos[0]++;
	}
	if(pos[0] < int(tokens.length) && tokens[pos[0]] == "]") pos[0]++;
}

void ParseNpcEntry(array<string>@ tokens, array<int>@ pos)
{
	if(pos[0] >= int(tokens.length) || tokens[pos[0]] != "{") { SkipNpcJsonValue(tokens, pos); return; }
	npcBunniesCore::npcBunny npc; SetNpcDefaults(npc); pos[0]++;
	while(pos[0] < int(tokens.length) && tokens[pos[0]] != "}")
	{
		string key = tokens[pos[0]++]; if(pos[0] < int(tokens.length) && tokens[pos[0]] == ":") pos[0]++;
		if(jjRegexMatch(key, "NPC", true) || jjRegexMatch(key, "Name", true)) npc.name = ReadNpcJsonString(tokens, pos);
		else if(jjRegexMatch(key, "Type", true) || jjRegexMatch(key, "Character", true)) npc.type = npcBunniesAnimations::getTypeFromTypeName(ReadNpcJsonString(tokens, pos));
		else if(jjRegexMatch(key, "Vars", true)) npc = ParseNpcVars(tokens, pos, npc);
		else if(jjRegexMatch(key, "GuideEnabled", true)) npc.guideEnabled = ReadNpcJsonBool(tokens, pos);
		else if(jjRegexMatch(key, "Guide", true)) npc = ParseNpcGuide(tokens, pos, npc);
		else if(jjRegexMatch(key, "FightingEnabled", true)) { npc.fightingAvailable = ReadNpcJsonBool(tokens, pos); npc.fightingEnabled = npc.fightingAvailable; }
		else if(jjRegexMatch(key, "FightingStyle", true)) npc.fightingStyle = npcBunniesFighting::ParseNpcFightingStyle(ReadNpcJsonString(tokens, pos));
		else if(jjRegexMatch(key, "Fighting", true)) npcBunniesFighting::ParseNpcFighting(tokens, pos, npc);
		else if(jjRegexMatch(key, "Shop", true)) npcBunniesShop::ParseNpcShop(tokens, pos, npc);
		else if(jjRegexMatch(key, "DialogueSets", true)) npc = ParseDialogueSets(tokens, pos, npc);
		else SkipNpcJsonValue(tokens, pos);
		if(pos[0] < int(tokens.length) && tokens[pos[0]] == ",") pos[0]++;
	}
	if(pos[0] < int(tokens.length) && tokens[pos[0]] == "}") pos[0]++;
	AddParsedNpc(npc);
}

void SetNpcDefaults(npcBunniesCore::npcBunny &out npc)
{
	npc.id = int(npcBunniesCore::npcBunnies.length); npc.name = "NPC"; npc.type = npcBunniesAnimations::getTypeFromTypeName("Jazz");
	npc.xTile = 1; npc.yTile = 1; npc.direction = 1;
	npc.sizeMode = 1;
	npc.moveSpeed = 1.0;
	npc.fur.c1 = 40; npc.fur.c2 = 32; npc.fur.c3 = 80; npc.fur.c4 = 64;
	npc.moveable = true; npc.move = false; npc.playerFollowing = -1; npc.followingTrainIndex = -1; npc.playerTalking = -1;
	npc.pushable = false;
	npc.traceMode = npcBunniesCore::NPC_TRACE_OFF; npc.traceColor = 24;
	npc.fightingAvailable = false; npc.fightingEnabled = false; npc.fightingStyle = npcBunniesCore::NpcFightingStyle::Complex; npc.fightingTargetObjectID = -1;
	npc.fightingLastScanTick = -9999; npc.fightingLastFireTick = -9999; npc.fightingAnimUntilTick = -1; npc.fightingDirection = 0; npc.fightingLastPlayerFireTick = -1;
	npc.hatFrame = -1; npc.scarfFrame = -1; npc.glassesFrame = -1;
	npc = npcBunniesCore::InitNpcQuestState(npc);
}

int ParseNpcTraceMode(const string &in value)
{
	if(jjRegexMatch(value, "^ON$", true) || jjRegexMatch(value, "^NORMAL$", true)) return npcBunniesCore::NPC_TRACE_NORMAL;
	if(jjRegexMatch(value, "^TRUE$", true)) return npcBunniesCore::NPC_TRACE_NORMAL;
	if(jjRegexMatch(value, "^FALSE$", true) || jjRegexMatch(value, "^OFF$", true)) return npcBunniesCore::NPC_TRACE_OFF;
	if(jjRegexMatch(value, "^INVERTED$", true) || jjRegexMatch(value, "^INVERT$", true)) return npcBunniesCore::NPC_TRACE_INVERTED;
	if(jjRegexMatch(value, "^CONST$", true) || jjRegexMatch(value, "^CONSTANT$", true)) return npcBunniesCore::NPC_TRACE_CONST;
	return npcBunniesCore::ClampNpcTraceMode(parseInt(value));
}

npcBunniesCore::npcBunny ParseNpcVars(array<string>@ tokens, array<int>@ pos, npcBunniesCore::npcBunny npc)
{
	if(pos[0] >= int(tokens.length) || tokens[pos[0]] != "{") { SkipNpcJsonValue(tokens, pos); return npc; }
	pos[0]++;
	while(pos[0] < int(tokens.length) && tokens[pos[0]] != "}")
	{
		string key = tokens[pos[0]++]; if(pos[0] < int(tokens.length) && tokens[pos[0]] == ":") pos[0]++;
		if(jjRegexMatch(key, "Type", true) || jjRegexMatch(key, "Character", true)) npc.type = npcBunniesAnimations::getTypeFromTypeName(ReadNpcJsonString(tokens, pos));
		else if(jjRegexMatch(key, "PosX", true)) npc.xTile = ReadNpcJsonFloat(tokens, pos);
		else if(jjRegexMatch(key, "PosY", true)) npc.yTile = ReadNpcJsonFloat(tokens, pos);
		else if(jjRegexMatch(key, "Direction", true)) npc.direction = ReadNpcJsonInt(tokens, pos);
		else if(jjRegexMatch(key, "Size", true)) npc.sizeMode = ParseNpcSizeMode(ReadNpcJsonString(tokens, pos));
		else if(jjRegexMatch(key, "Speed", true) || jjRegexMatch(key, "MoveSpeed", true)) npc.moveSpeed = ClampNpcMoveSpeed(ReadNpcJsonFloat(tokens, pos));
		else if(jjRegexMatch(key, "FurColor1", true)) npc.fur.c1 = ReadNpcJsonInt(tokens, pos);
		else if(jjRegexMatch(key, "FurColor2", true)) npc.fur.c2 = ReadNpcJsonInt(tokens, pos);
		else if(jjRegexMatch(key, "FurColor3", true)) npc.fur.c3 = ReadNpcJsonInt(tokens, pos);
		else if(jjRegexMatch(key, "FurColor4", true)) npc.fur.c4 = ReadNpcJsonInt(tokens, pos);
		else if(jjRegexMatch(key, "Moveable", true)) npc.moveable = ReadNpcJsonBool(tokens, pos);
		else if(jjRegexMatch(key, "Pushable", true)) npc.pushable = ReadNpcJsonBool(tokens, pos);
		else if(jjRegexMatch(key, "^TraceColor$", true)) npc.traceColor = npcBunniesCore::ClampNpcTraceColor(ReadNpcJsonInt(tokens, pos));
		else if(jjRegexMatch(key, "^TraceMode$", true) || jjRegexMatch(key, "^Trace$", true)) npc.traceMode = ParseNpcTraceMode(ReadNpcJsonString(tokens, pos));
		else if(jjRegexMatch(key, "GuideEnabled", true)) npc.guideEnabled = ReadNpcJsonBool(tokens, pos);
		else if(jjRegexMatch(key, "Guide", true)) npc = ParseNpcGuide(tokens, pos, npc);
		else if(jjRegexMatch(key, "FightingEnabled", true)) { npc.fightingAvailable = ReadNpcJsonBool(tokens, pos); npc.fightingEnabled = npc.fightingAvailable; }
		else if(jjRegexMatch(key, "FightingStyle", true)) npc.fightingStyle = npcBunniesFighting::ParseNpcFightingStyle(ReadNpcJsonString(tokens, pos));
		else if(jjRegexMatch(key, "Fighting", true)) npcBunniesFighting::ParseNpcFighting(tokens, pos, npc);
		else if(jjRegexMatch(key, "Hat", true)) npc.hatFrame = ReadNpcAccessoryFrame(tokens, pos);
		else if(jjRegexMatch(key, "Scarf", true)) npc.scarfFrame = ReadNpcAccessoryFrame(tokens, pos);
		else if(jjRegexMatch(key, "Glasses", true)) npc.glassesFrame = ReadNpcAccessoryFrame(tokens, pos);
		else if(jjRegexMatch(key, "Shop", true)) npcBunniesShop::ParseNpcShop(tokens, pos, npc);
		else SkipNpcJsonValue(tokens, pos);
		if(pos[0] < int(tokens.length) && tokens[pos[0]] == ",") pos[0]++;
	}
	if(pos[0] < int(tokens.length) && tokens[pos[0]] == "}") pos[0]++;
	return npc;
}

npcBunniesCore::npcBunny ParseNpcGuide(array<string>@ tokens, array<int>@ pos, npcBunniesCore::npcBunny npc)
{
	if(pos[0] >= int(tokens.length) || tokens[pos[0]] != "{") { npc.guideEnabled = ReadNpcJsonBool(tokens, pos); return npc; }
	pos[0]++;
	while(pos[0] < int(tokens.length) && tokens[pos[0]] != "}")
	{
		string key = tokens[pos[0]++]; if(pos[0] < int(tokens.length) && tokens[pos[0]] == ":") pos[0]++;
		if(jjRegexMatch(key, "Enabled", true) || key == "On") npc.guideEnabled = ReadNpcJsonBool(tokens, pos);
		else if(jjRegexMatch(key, "Paths", true)) ParseGuidePaths(tokens, pos, npc);
		else if(jjRegexMatch(key, "EndLine", true)) npc.guideEndLine = ReadNpcJsonString(tokens, pos);
		else if(jjRegexMatch(key, "AlreadyAtDestinationLine", true) || jjRegexMatch(key, "AlreadyLine", true)) npc.guideAlreadyAtDestinationLine = ReadNpcJsonString(tokens, pos);
		else SkipNpcJsonValue(tokens, pos);
		if(pos[0] < int(tokens.length) && tokens[pos[0]] == ",") pos[0]++;
	}
	if(pos[0] < int(tokens.length) && tokens[pos[0]] == "}") pos[0]++;
	if(npc.guidePaths.length > 0) npc.guideEnabled = true;
	return npc;
}

void ParseGuidePaths(array<string>@ tokens, array<int>@ pos, npcBunniesCore::npcBunny &inout npc)
{
	if(pos[0] >= int(tokens.length) || tokens[pos[0]] != "[") { SkipNpcJsonValue(tokens, pos); return; }
	pos[0]++;
	while(pos[0] < int(tokens.length) && tokens[pos[0]] != "]")
	{
		npc.guidePaths.insertLast(ParseGuidePath(tokens, pos));
		if(pos[0] < int(tokens.length) && tokens[pos[0]] == ",") pos[0]++;
	}
	if(pos[0] < int(tokens.length) && tokens[pos[0]] == "]") pos[0]++;
}

npcBunniesCore::GuidePath@ ParseGuidePath(array<string>@ tokens, array<int>@ pos)
{
	npcBunniesCore::GuidePath@ path = @npcBunniesCore::GuidePath();
	if(pos[0] >= int(tokens.length) || tokens[pos[0]] != "{") { SkipNpcJsonValue(tokens, pos); return path; }
	pos[0]++;
	while(pos[0] < int(tokens.length) && tokens[pos[0]] != "}")
	{
		string key = tokens[pos[0]++]; if(pos[0] < int(tokens.length) && tokens[pos[0]] == ":") pos[0]++;
		if(jjRegexMatch(key, "Type", true))
		{
			string type = ReadNpcJsonString(tokens, pos);
			path.isTeleport = jjRegexMatch(type, "Teleport", true);
		}
		else if(jjRegexMatch(key, "Line", true) || jjRegexMatch(key, "StartLine", true)) path.line = ReadNpcJsonString(tokens, pos);
		else if(jjRegexMatch(key, "Points", true) || jjRegexMatch(key, "Steps", true) || jjRegexMatch(key, "Path", true)) ParseGuideSteps(tokens, pos, path);
		else if(key == "X" || jjRegexMatch(key, "TeleportX", true)) { path.teleportX = ReadNpcJsonInt(tokens, pos); path.isTeleport = true; }
		else if(key == "Y" || jjRegexMatch(key, "TeleportY", true)) { path.teleportY = ReadNpcJsonInt(tokens, pos); path.isTeleport = true; }
		else if(jjRegexMatch(key, "Direction", true)) path.teleportDirection = ReadNpcJsonInt(tokens, pos) < 0 ? -1 : 1;
		else SkipNpcJsonValue(tokens, pos);
		if(pos[0] < int(tokens.length) && tokens[pos[0]] == ",") pos[0]++;
	}
	if(pos[0] < int(tokens.length) && tokens[pos[0]] == "}") pos[0]++;
	return path;
}

void ParseGuideSteps(array<string>@ tokens, array<int>@ pos, npcBunniesCore::GuidePath@ path)
{
	if(pos[0] >= int(tokens.length) || tokens[pos[0]] != "[") { SkipNpcJsonValue(tokens, pos); return; }
	pos[0]++;
	while(pos[0] < int(tokens.length) && tokens[pos[0]] != "]")
	{
		path.steps.insertLast(ParseGuideStep(tokens, pos));
		if(pos[0] < int(tokens.length) && tokens[pos[0]] == ",") pos[0]++;
	}
	if(pos[0] < int(tokens.length) && tokens[pos[0]] == "]") pos[0]++;
}

npcBunniesCore::GuideStep@ ParseGuideStep(array<string>@ tokens, array<int>@ pos)
{
	npcBunniesCore::GuideStep@ step = @npcBunniesCore::GuideStep();
	if(pos[0] < int(tokens.length) && tokens[pos[0]] == "[")
	{
		pos[0]++;
		step.x = ReadNpcJsonInt(tokens, pos);
		if(pos[0] < int(tokens.length) && tokens[pos[0]] == ",") pos[0]++;
		step.y = ReadNpcJsonInt(tokens, pos);
		while(pos[0] < int(tokens.length) && tokens[pos[0]] != "]") pos[0]++;
		if(pos[0] < int(tokens.length) && tokens[pos[0]] == "]") pos[0]++;
		return step;
	}
	if(pos[0] >= int(tokens.length) || tokens[pos[0]] != "{") { SkipNpcJsonValue(tokens, pos); return step; }
	pos[0]++;
	while(pos[0] < int(tokens.length) && tokens[pos[0]] != "}")
	{
		string key = tokens[pos[0]++]; if(pos[0] < int(tokens.length) && tokens[pos[0]] == ":") pos[0]++;
		if(key == "X" || jjRegexMatch(key, "DX", true)) step.x = ReadNpcJsonInt(tokens, pos);
		else if(key == "Y" || jjRegexMatch(key, "DY", true)) step.y = ReadNpcJsonInt(tokens, pos);
		else SkipNpcJsonValue(tokens, pos);
		if(pos[0] < int(tokens.length) && tokens[pos[0]] == ",") pos[0]++;
	}
	if(pos[0] < int(tokens.length) && tokens[pos[0]] == "}") pos[0]++;
	return step;
}
npcBunniesCore::npcBunny ParseDialogueSets(array<string>@ tokens, array<int>@ pos, npcBunniesCore::npcBunny npc)
{
	if(pos[0] >= int(tokens.length) || tokens[pos[0]] != "[") { SkipNpcJsonValue(tokens, pos); return npc; }
	pos[0]++;
	while(pos[0] < int(tokens.length) && tokens[pos[0]] != "]") { npc.dialogueSets.insertLast(ParseDialogueSet(tokens, pos)); if(pos[0] < int(tokens.length) && tokens[pos[0]] == ",") pos[0]++; }
	if(pos[0] < int(tokens.length) && tokens[pos[0]] == "]") pos[0]++;
	SanitizeNpcDialogueAutoContinue(npc);
	return npc;
}

int ParsedDialogueAutoContinueTarget(npcBunniesCore::npcBunny &in npc, int sourceSet, int autoContinueSet)
{
	if(autoContinueSet == -2) return -1;
	if(autoContinueSet == -1) return sourceSet + 1;
	return autoContinueSet;
}

bool ParsedDialogueAutoContinueWouldLoop(npcBunniesCore::npcBunny &in npc, int sourceSet, int candidate)
{
	int target = ParsedDialogueAutoContinueTarget(npc, sourceSet, candidate);
	for(int guard = 0; guard < int(npc.dialogueSets.length) + 1; guard++)
	{
		if(target < 0 || target >= int(npc.dialogueSets.length)) return false;
		if(target == sourceSet) return true;
		npcBunniesDialogue::DialogueSet@ set = npc.dialogueSets[target];
		if(set is null || set.choicesAnswerMethod != npcBunniesCore::DialogueAction::Normal || set.autoContinueSet == -2) return false;
		target = ParsedDialogueAutoContinueTarget(npc, target, set.autoContinueSet);
	}
	return true;
}

void SanitizeNpcDialogueAutoContinue(npcBunniesCore::npcBunny &inout npc)
{
	for(uint i = 0; i < npc.dialogueSets.length; i++)
	{
		npcBunniesDialogue::DialogueSet@ set = npc.dialogueSets[i];
		if(set !is null && set.choicesAnswerMethod == npcBunniesCore::DialogueAction::Normal && set.autoContinueSet != -2 && ParsedDialogueAutoContinueWouldLoop(npc, int(i), set.autoContinueSet))
			set.autoContinueSet = -2;
	}
}

npcBunniesDialogue::DialogueSet@ ParseDialogueSet(array<string>@ tokens, array<int>@ pos)
{
	npcBunniesDialogue::DialogueSet@ set = @npcBunniesDialogue::DialogueSet(array<npcBunniesDialogue::DialogueLine@>(), array<npcBunniesDialogue::Choice@>(), npcBunniesCore::DialogueAction::Normal);
	if(pos[0] >= int(tokens.length) || tokens[pos[0]] != "{") { SkipNpcJsonValue(tokens, pos); return set; }
	pos[0]++;
	while(pos[0] < int(tokens.length) && tokens[pos[0]] != "}")
	{
		string key = tokens[pos[0]++]; if(pos[0] < int(tokens.length) && tokens[pos[0]] == ":") pos[0]++;
		if(jjRegexMatch(key, "DialogueLines", true) || jjRegexMatch(key, "Lines", true)) ParseDialogueLines(tokens, pos, set);
		else if(jjRegexMatch(key, "Type", true)) set.choicesAnswerMethod = ParseDialogueAction(ReadNpcJsonString(tokens, pos));
		else if(jjRegexMatch(key, "Speaker", true) || jjRegexMatch(key, "SpeakerNpc", true) || jjRegexMatch(key, "SpeakerIndex", true)) set.speakerNpcIndex = ParseDialogueSpeakerIndex(ReadNpcJsonString(tokens, pos));
		else if(key == "AutoContinueSet") set.autoContinueSet = ReadNpcJsonInt(tokens, pos);
		else if(jjRegexMatch(key, "AutoContinue", true)) set.autoContinueSet = ParseDialogueAutoContinue(ReadNpcJsonString(tokens, pos));
		else if(jjRegexMatch(key, "Frame", true) || jjRegexMatch(key, "BigFrame", true) || jjRegexMatch(key, "DialogueFrame", true)) set.bigFrame = ReadNpcJsonInt(tokens, pos);
		else if(jjRegexMatch(key, "GoToSet", true)) set.goToSet = ReadNpcJsonInt(tokens, pos);
		else if(jjRegexMatch(key, "QuestInProgressLine", true)) set.questInProgressLine = ReadNpcJsonString(tokens, pos);
		else if(jjRegexMatch(key, "QuestFinishedLine", true) || jjRegexMatch(key, "QuestCompletedLine", true)) set.questCompletedLine = ReadNpcJsonString(tokens, pos);
		else if(jjRegexMatch(key, "RespawnMonsters", true) || jjRegexMatch(key, "RespawnQuestMonsters", true)) set.respawnMonsters = ReadNpcJsonBool(tokens, pos);
		else if(jjRegexMatch(key, "QuestInProgress", true)) set.questInProgressSet = ReadNpcJsonInt(tokens, pos);
		else if(jjRegexMatch(key, "QuestCompleted", true)) set.questCompletedSet = ReadNpcJsonInt(tokens, pos);
		else if(jjRegexMatch(key, "Quests", true) || jjRegexMatch(key, "Quest", true)) ParseQuestArray(tokens, pos, set);
		else if(jjRegexMatch(key, "Choices", true)) { if(set.choicesAnswerMethod != npcBunniesCore::DialogueAction::Quest) set.choicesAnswerMethod = npcBunniesCore::DialogueAction::Choice; ParseChoiceArray(tokens, pos, set); }
		else if(jjRegexMatch(key, "TextAnswers", true)) { if(set.choicesAnswerMethod != npcBunniesCore::DialogueAction::Quest) set.choicesAnswerMethod = npcBunniesCore::DialogueAction::Answer; ParseChoiceArray(tokens, pos, set); }
		else if(jjRegexMatch(key, "Reward", true) || jjRegexMatch(key, "Rewards", true)) ParseRewardArray(tokens, pos, set);
		else SkipNpcJsonValue(tokens, pos);
		if(pos[0] < int(tokens.length) && tokens[pos[0]] == ",") pos[0]++;
	}
	if(pos[0] < int(tokens.length) && tokens[pos[0]] == "}") pos[0]++;
	if(set.quests.length > 0 && set.choicesAnswerMethod == npcBunniesCore::DialogueAction::Normal) set.choicesAnswerMethod = npcBunniesCore::DialogueAction::Quest;
	set.conditionsExist = set.quests.length > 0;
	return set;
}

void ParseDialogueLines(array<string>@ tokens, array<int>@ pos, npcBunniesDialogue::DialogueSet@ set)
{
	if(pos[0] >= int(tokens.length) || tokens[pos[0]] != "[") { SkipNpcJsonValue(tokens, pos); return; }
	pos[0]++;
	while(pos[0] < int(tokens.length) && tokens[pos[0]] != "]")
	{
		npcBunniesDialogue::DialogueLine@ line = @npcBunniesDialogue::DialogueLine();
		if(tokens[pos[0]] == "{")
		{
			pos[0]++;
			while(pos[0] < int(tokens.length) && tokens[pos[0]] != "}")
			{
				string key = tokens[pos[0]++]; if(pos[0] < int(tokens.length) && tokens[pos[0]] == ":") pos[0]++;
				if(jjRegexMatch(key, "SpokenByNpc", true)) line.isNpc = ReadNpcJsonBool(tokens, pos) ? 0 : 1;
		else if(jjRegexMatch(key, "SpeakIndex", true)) line.personId = ReadNpcJsonInt(tokens, pos);
		else if(jjRegexMatch(key, "Line", true) || jjRegexMatch(key, "Text", true)) line.text = ReadNpcJsonString(tokens, pos);
				else SkipNpcJsonValue(tokens, pos);
				if(pos[0] < int(tokens.length) && tokens[pos[0]] == ",") pos[0]++;
			}
			if(pos[0] < int(tokens.length) && tokens[pos[0]] == "}") pos[0]++;
		}
		else line.text = ReadNpcJsonString(tokens, pos);
		set.dialogue.insertLast(line);
		if(pos[0] < int(tokens.length) && tokens[pos[0]] == ",") pos[0]++;
	}
	if(pos[0] < int(tokens.length) && tokens[pos[0]] == "]") pos[0]++;
}

void ParseChoiceArray(array<string>@ tokens, array<int>@ pos, npcBunniesDialogue::DialogueSet@ set)
{
	if(pos[0] < int(tokens.length) && tokens[pos[0]] == "[") pos[0]++; else { SkipNpcJsonValue(tokens, pos); return; }
	while(pos[0] < int(tokens.length) && tokens[pos[0]] != "]")
	{
		npcBunniesDialogue::Choice@ choice = @npcBunniesDialogue::Choice("");
		if(tokens[pos[0]] == "{")
		{
			pos[0]++;
			while(pos[0] < int(tokens.length) && tokens[pos[0]] != "}")
			{
				string key = tokens[pos[0]++]; if(pos[0] < int(tokens.length) && tokens[pos[0]] == ":") pos[0]++;
				if(jjRegexMatch(key, "Text", true)) choice.choiceText = ReadNpcJsonString(tokens, pos);
		else if(jjRegexMatch(key, "GoToSet", true)) choice.goToSet = ReadNpcJsonInt(tokens, pos);
				else SkipNpcJsonValue(tokens, pos);
				if(pos[0] < int(tokens.length) && tokens[pos[0]] == ",") pos[0]++;
			}
			if(pos[0] < int(tokens.length) && tokens[pos[0]] == "}") pos[0]++;
		}
		set.choices.insertLast(choice);
		if(pos[0] < int(tokens.length) && tokens[pos[0]] == ",") pos[0]++;
	}
	if(pos[0] < int(tokens.length) && tokens[pos[0]] == "]") pos[0]++;
}

void ParseQuestArray(array<string>@ tokens, array<int>@ pos, npcBunniesDialogue::DialogueSet@ set)
{
	if(pos[0] < int(tokens.length) && tokens[pos[0]] == "[") pos[0]++; else { SkipNpcJsonValue(tokens, pos); return; }
	while(pos[0] < int(tokens.length) && tokens[pos[0]] != "]") { set.quests.insertLast(ParseQuest(tokens, pos)); if(pos[0] < int(tokens.length) && tokens[pos[0]] == ",") pos[0]++; }
	if(pos[0] < int(tokens.length) && tokens[pos[0]] == "]") pos[0]++;
}

npcBunniesQuests::NpcQuest@ ParseQuest(array<string>@ tokens, array<int>@ pos)
{
	npcBunniesQuests::NpcQuest@ quest = @npcBunniesQuests::NpcQuest();
	if(pos[0] >= int(tokens.length) || tokens[pos[0]] != "{") { SkipNpcJsonValue(tokens, pos); return quest; }
	pos[0]++;
	while(pos[0] < int(tokens.length) && tokens[pos[0]] != "}")
	{
		string key = tokens[pos[0]++]; if(pos[0] < int(tokens.length) && tokens[pos[0]] == ":") pos[0]++;
		if(jjRegexMatch(key, "QuestType", true)) quest.questType = npcBunniesQuests::ParseQuestType(ReadNpcJsonString(tokens, pos));
		else if(jjRegexMatch(key, "Amount", true)) quest.amount = ReadNpcJsonInt(tokens, pos);
		else if(jjRegexMatch(key, "Gem", true) || jjRegexMatch(key, "GemType", true)) quest.gemType = npcBunniesQuests::ParseGemType(ReadNpcJsonString(tokens, pos));
		else if(jjRegexMatch(key, "Monster", true) || jjRegexMatch(key, "MonsterType", true) || jjRegexMatch(key, "Boss", true) || jjRegexMatch(key, "ObjectType", true) || jjRegexMatch(key, "EventID", true))
		{
			int parsedEventID = npcBunniesQuests::ParseDefeatEventID(ReadNpcJsonString(tokens, pos));
			if(parsedEventID > 0) quest.objectType = parsedEventID;
		}
		else if(jjRegexMatch(key, "Icon", true) || jjRegexMatch(key, "Animation", true)) ParseNpcUiAnimation(tokens, pos, quest.icon, quest.iconSpecified, quest.iconHidden);
		else if(jjRegexMatch(key, "CountAfterQuestIsStarted", true)) quest.countAfterQuestIsStarted = ReadNpcJsonBool(tokens, pos);
		else if(jjRegexMatch(key, "CompleteQuestTogether", true)) quest.completeQuestTogether = ReadNpcJsonBool(tokens, pos);
		else if(jjRegexMatch(key, "Respawn", true)) quest.respawn = ReadNpcJsonBool(tokens, pos);
		else SkipNpcJsonValue(tokens, pos);
		if(pos[0] < int(tokens.length) && tokens[pos[0]] == ",") pos[0]++;
	}
	if(pos[0] < int(tokens.length) && tokens[pos[0]] == "}") pos[0]++;
	if(quest.questType == npcBunniesQuests::QuestType::DefeatMonster || quest.questType == npcBunniesQuests::QuestType::DefeatBoss) npcBunniesQuests::TrackDefeatEventID(quest.objectType, !quest.countAfterQuestIsStarted);
	return quest;
}

void ParseRewardArray(array<string>@ tokens, array<int>@ pos, npcBunniesDialogue::DialogueSet@ set)
{
	if(pos[0] < int(tokens.length) && tokens[pos[0]] == "[") pos[0]++; else { SkipNpcJsonValue(tokens, pos); return; }
	while(pos[0] < int(tokens.length) && tokens[pos[0]] != "]") { set.rewards.insertLast(ParseReward(tokens, pos)); if(pos[0] < int(tokens.length) && tokens[pos[0]] == ",") pos[0]++; }
	if(pos[0] < int(tokens.length) && tokens[pos[0]] == "]") pos[0]++;
}

npcBunniesQuests::NpcReward@ ParseReward(array<string>@ tokens, array<int>@ pos)
{
	npcBunniesQuests::NpcReward@ reward = @npcBunniesQuests::NpcReward();
	if(pos[0] >= int(tokens.length) || tokens[pos[0]] != "{") { SkipNpcJsonValue(tokens, pos); return reward; }
	pos[0]++;
	while(pos[0] < int(tokens.length) && tokens[pos[0]] != "}")
	{
		string key = tokens[pos[0]++]; if(pos[0] < int(tokens.length) && tokens[pos[0]] == ":") pos[0]++;
		if(jjRegexMatch(key, "RewardType", true)) reward.rewardType = npcBunniesQuests::ParseRewardType(ReadNpcJsonString(tokens, pos));
		else if(jjRegexMatch(key, "Amount", true)) reward.amount = ReadNpcJsonInt(tokens, pos);
		else if(jjRegexMatch(key, "Gem", true) || jjRegexMatch(key, "GemType", true)) reward.gemType = npcBunniesQuests::ParseGemType(ReadNpcJsonString(tokens, pos));
		else if(jjRegexMatch(key, "Ammo", true) || jjRegexMatch(key, "AmmoType", true) || jjRegexMatch(key, "Weapon", true)) reward.weaponType = npcBunniesQuests::ParseWeaponType(ReadNpcJsonString(tokens, pos));
		else if(jjRegexMatch(key, "Icon", true) || jjRegexMatch(key, "Animation", true)) ParseNpcUiAnimation(tokens, pos, reward.icon, reward.iconSpecified, reward.iconHidden);
		else if(jjRegexMatch(key, "X", true)) reward.x = ReadNpcJsonFloat(tokens, pos);
		else if(jjRegexMatch(key, "Y", true)) reward.y = ReadNpcJsonFloat(tokens, pos);
		else if(jjRegexMatch(key, "Moveable", true)) reward.moveable = ReadNpcJsonBool(tokens, pos);
		else if(jjRegexMatch(key, "NpcIndex", true)) reward.npcIndex = ReadNpcJsonInt(tokens, pos);
		else if(jjRegexMatch(key, "GuidePath", true) || jjRegexMatch(key, "Path", true)) reward.guidePathIndex = ReadNpcJsonInt(tokens, pos) - 1;
		else if(jjRegexMatch(key, "TriggerState", true) || jjRegexMatch(key, "State", true) || jjRegexMatch(key, "On", true)) reward.triggerState = ReadNpcJsonBool(tokens, pos);
		else if(jjRegexMatch(key, "Trigger", true) || jjRegexMatch(key, "TriggerId", true)) reward.triggerId = ReadNpcJsonInt(tokens, pos);
		else SkipNpcJsonValue(tokens, pos);
		if(pos[0] < int(tokens.length) && tokens[pos[0]] == ",") pos[0]++;
	}
	if(pos[0] < int(tokens.length) && tokens[pos[0]] == "}") pos[0]++;
	return reward;
}

void AddParsedNpc(npcBunniesCore::npcBunny npc)
{
	npc.id = int(npcBunniesCore::npcBunnies.length);
	int objId = jjAddObject(OBJECT::CHERRY, npc.xTile * 32, npc.yTile * 32);
	jjOBJ@ obj = jjObjects[objId];
	obj.behavior = npcBunniesMovement::Npc(); obj.direction = npc.direction; obj.scriptedCollisions = true; obj.playerHandling = HANDLING::SPECIAL;
	obj.var[0] = npc.id + 1;
	npcBunniesAnimations::LoadNpcSimpleAnimSet(npc.type);
	if(npcBunniesAnimations::IsNpcStaticType(npc.type)) { npc.moveable = false; npc.pushable = false; npc.guideActive = false; npc.playerFollowing = -1; npc.fightingAvailable = false; npc.fightingEnabled = false; }
	npc.objectID = obj.objectID; npc.fallToX = obj.xPos; npc.fallToY = obj.yPos;
	npcBunniesCore::npcObjectIds.set(obj.objectID + '', npc.id);
	npcBunniesCore::npcBunnies.insertLast(npc);
}

string ReadNpcJsonString(array<string>@ tokens, array<int>@ pos) { if(pos[0] >= int(tokens.length)) return ""; return tokens[pos[0]++]; }
int ReadNpcJsonInt(array<string>@ tokens, array<int>@ pos) { return parseInt(ReadNpcJsonString(tokens, pos)); }
float ReadNpcJsonFloat(array<string>@ tokens, array<int>@ pos) { return parseFloat(ReadNpcJsonString(tokens, pos)); }
bool ReadNpcJsonBool(array<string>@ tokens, array<int>@ pos) { string value = ReadNpcJsonString(tokens, pos); return jjRegexMatch(value, "true", true) || value == "1"; }
float ClampNpcMoveSpeed(float speed)
{
	if(speed < 0.1) return 0.1;
	if(speed > 5.0) return 5.0;
	return speed;
}
int ReadNpcAccessoryFrame(array<string>@ tokens, array<int>@ pos)
{
	string value = ReadNpcJsonString(tokens, pos);
	if(jjRegexMatch(value, "No", true) || jjRegexMatch(value, "None", true) || value == "-1") return -1;
	return parseInt(value);
}

int ParseNpcSizeMode(const string &in value)
{
	if(jjRegexMatch(value, "Tiny", true) || value == "0") return 0;
	if(jjRegexMatch(value, "Giant", true) || value == "2") return 2;
	return 1;
}

int ParseDialogueSpeakerIndex(const string &in value)
{
	if(value == "" || jjRegexMatch(value, "Default", true) || jjRegexMatch(value, "This", true)) return -1;
	int index = parseInt(value) - 1;
	return index < 0 ? -1 : index;
}

int ParseDialogueAutoContinue(const string &in value)
{
	if(value == "" || value == "-" || value == "None" || value == "none" || value == "Blank" || value == "blank") return -2;
	if(value == "next" || value == "Next" || value == "next set" || value == "Next Set") return -1;
	int index = value.length > 4 && (value.substr(0, 4) == "set " || value.substr(0, 4) == "Set ") ? parseInt(value.substr(4)) - 1 : parseInt(value) - 1;
	return index < 0 ? -2 : index;
}

SPRITE::Mode ParseNpcSpriteMode(const string &in value)
{
	if(jjRegexMatch(value, "PalShift", true) || jjRegexMatch(value, "PALSHIFT", true)) return SPRITE::PALSHIFT;
	if(jjRegexMatch(value, "Gem", true)) return SPRITE::GEM;
	if(jjRegexMatch(value, "Player", true)) return SPRITE::PLAYER;
	if(jjRegexMatch(value, "SingleColor", true) || jjRegexMatch(value, "SingleColour", true)) return SPRITE::SINGLECOLOR;
	if(jjRegexMatch(value, "Shadow", true)) return SPRITE::SHADOW;
	return SPRITE::NORMAL;
}

void ParseNpcUiAnimation(array<string>@ tokens, array<int>@ pos, npcBunniesAnimations::NpcUiAnimation &inout icon, bool &out specified, bool &out hidden)
{
	specified = true;
	hidden = false;
	npcBunniesAnimations::SetNpcUiAnimation(icon, 0, 0, 0, 1, 6);
	if(pos[0] >= int(tokens.length)) return;
	if(tokens[pos[0]] != "{")
	{
		string value = ReadNpcJsonString(tokens, pos);
		if(jjRegexMatch(value, "None", true) || jjRegexMatch(value, "NoIcon", true) || jjRegexMatch(value, "Hidden", true) || jjRegexMatch(value, "Off", true))
		{
			hidden = true;
			icon.hidden = true;
		}
		return;
	}
	pos[0]++;
	while(pos[0] < int(tokens.length) && tokens[pos[0]] != "}")
	{
		string key = tokens[pos[0]++]; if(pos[0] < int(tokens.length) && tokens[pos[0]] == ":") pos[0]++;
		if(jjRegexMatch(key, "Hidden", true) || jjRegexMatch(key, "Hide", true) || jjRegexMatch(key, "NoIcon", true)) hidden = ReadNpcJsonBool(tokens, pos);
		else if(jjRegexMatch(key, "CustomSlot", true) || jjRegexMatch(key, "CustomAnimSet", true)) icon.animSet = ANIM::CUSTOM[ReadNpcJsonInt(tokens, pos)];
		else if(jjRegexMatch(key, "AnimSet", true) || jjRegexMatch(key, "AnimationSet", true)) icon.animSet = ReadNpcJsonInt(tokens, pos);
		else if(jjRegexMatch(key, "FrameCount", true) || jjRegexMatch(key, "Frames", true)) icon.frameCount = ReadNpcJsonInt(tokens, pos);
		else if(jjRegexMatch(key, "TicksPerFrame", true) || jjRegexMatch(key, "Speed", true)) icon.ticksPerFrame = ReadNpcJsonInt(tokens, pos);
		else if(jjRegexMatch(key, "FirstFrame", true) || key == "Frame") icon.firstFrame = ReadNpcJsonInt(tokens, pos);
		else if(jjRegexMatch(key, "Anim", true) || key == "Animation") icon.anim = ReadNpcJsonInt(tokens, pos);
		else if(jjRegexMatch(key, "SpriteMode", true) || jjRegexMatch(key, "Mode", true)) icon.spriteMode = ParseNpcSpriteMode(ReadNpcJsonString(tokens, pos));
		else if(jjRegexMatch(key, "Param", true) || jjRegexMatch(key, "Palette", true) || jjRegexMatch(key, "PalShift", true)) icon.param = ReadNpcJsonInt(tokens, pos);
		else if(jjRegexMatch(key, "OffsetX", true) || jjRegexMatch(key, "XOffset", true)) icon.offsetX = ReadNpcJsonInt(tokens, pos);
		else if(jjRegexMatch(key, "OffsetY", true) || jjRegexMatch(key, "YOffset", true)) icon.offsetY = ReadNpcJsonInt(tokens, pos);
		else SkipNpcJsonValue(tokens, pos);
		if(pos[0] < int(tokens.length) && tokens[pos[0]] == ",") pos[0]++;
	}
	if(pos[0] < int(tokens.length) && tokens[pos[0]] == "}") pos[0]++;
	icon.hidden = hidden;
}
void SkipNpcJsonValue(array<string>@ tokens, array<int>@ pos)
{
	if(pos[0] >= int(tokens.length)) return;
	if(tokens[pos[0]] == "{" || tokens[pos[0]] == "[")
	{
		string open = tokens[pos[0]], close = open == "{" ? "}" : "]";
		int depth = 1; pos[0]++;
		while(pos[0] < int(tokens.length) && depth > 0) { if(tokens[pos[0]] == open) depth++; else if(tokens[pos[0]] == close) depth--; pos[0]++; }
	}
	else pos[0]++;
}

int ParseDialogueAction(const string &in value)
{
	if(value == "1" || jjRegexMatch(value, "Choice", true) || jjRegexMatch(value, "Choices", true)) return npcBunniesCore::DialogueAction::Choice;
	if(value == "2" || jjRegexMatch(value, "Answer", true) || jjRegexMatch(value, "TextAnswer", true)) return npcBunniesCore::DialogueAction::Answer;
	if(value == "3" || jjRegexMatch(value, "Quest", true)) return npcBunniesCore::DialogueAction::Quest;
	return npcBunniesCore::DialogueAction::Normal;
}


//----------------------------------------------------------------------------------------------------
// NPC SHOP REGION
// Summary: Shop items are JSON-defined rewards sold for coin and/or gem prices. The shop displays one or two columns,
// supports player direction-key navigation, and confirms purchases with Select, Fire, or Enter.

}