/* * Copyright (C) 2004-2015 L2J Server * * This file is part of L2J Server. * * L2J Server is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * L2J Server is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.l2jserver.gameserver.data.xml.impl; import java.io.File; import java.util.ArrayList; import java.util.Collections; import java.util.EnumMap; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.function.Predicate; import java.util.stream.Collectors; import org.w3c.dom.Document; import org.w3c.dom.NamedNodeMap; import org.w3c.dom.Node; import com.l2jserver.Config; import com.l2jserver.gameserver.datatables.SkillData; import com.l2jserver.gameserver.enums.AISkillScope; import com.l2jserver.gameserver.model.StatsSet; import com.l2jserver.gameserver.model.actor.templates.L2NpcTemplate; import com.l2jserver.gameserver.model.base.ClassId; import com.l2jserver.gameserver.model.drops.DropListScope; import com.l2jserver.gameserver.model.drops.GeneralDropItem; import com.l2jserver.gameserver.model.drops.GroupedGeneralDropItem; import com.l2jserver.gameserver.model.drops.IDropItem; import com.l2jserver.gameserver.model.effects.L2EffectType; import com.l2jserver.gameserver.model.holders.MinionHolder; import com.l2jserver.gameserver.model.holders.SkillHolder; import com.l2jserver.gameserver.model.skills.Skill; import com.l2jserver.gameserver.util.Util; import com.l2jserver.util.data.xml.IXmlReader; /** * NPC data parser. * @author NosBit */ public class NpcData implements IXmlReader { private final Map _npcs = new ConcurrentHashMap<>(); private final Map _clans = new ConcurrentHashMap<>(); private MinionData _minionData; protected NpcData() { load(); } @Override public synchronized void load() { _minionData = new MinionData(); parseDatapackDirectory("data/stats/npcs", false); LOGGER.info("{}: Loaded {} NPCs.", getClass().getSimpleName(), _npcs.size()); if (Config.CUSTOM_NPC_DATA) { final int npcCount = _npcs.size(); parseDatapackDirectory("data/stats/npcs/custom", true); LOGGER.info("{}: Loaded {} custom NPCs.", getClass().getSimpleName(), (_npcs.size() - npcCount)); } _minionData = null; loadNpcsSkillLearn(); } @Override public void parseDocument(Document doc, File f) { for (Node node = doc.getFirstChild(); node != null; node = node.getNextSibling()) { if ("list".equalsIgnoreCase(node.getNodeName())) { for (Node list_node = node.getFirstChild(); list_node != null; list_node = list_node.getNextSibling()) { if ("npc".equalsIgnoreCase(list_node.getNodeName())) { NamedNodeMap attrs = list_node.getAttributes(); final StatsSet set = new StatsSet(); final int npcId = parseInteger(attrs, "id"); Map parameters = null; Map skills = null; Set clans = null; Set ignoreClanNpcIds = null; Map> dropLists = null; set.set("id", npcId); set.set("displayId", parseInteger(attrs, "displayId")); set.set("level", parseByte(attrs, "level")); set.set("type", parseString(attrs, "type")); set.set("name", parseString(attrs, "name")); set.set("usingServerSideName", parseBoolean(attrs, "usingServerSideName")); set.set("title", parseString(attrs, "title")); set.set("usingServerSideTitle", parseBoolean(attrs, "usingServerSideTitle")); for (Node npc_node = list_node.getFirstChild(); npc_node != null; npc_node = npc_node.getNextSibling()) { attrs = npc_node.getAttributes(); switch (npc_node.getNodeName().toLowerCase()) { case "parameters": { if (parameters == null) { parameters = new HashMap<>(); } for (Node parameters_node = npc_node.getFirstChild(); parameters_node != null; parameters_node = parameters_node.getNextSibling()) { attrs = parameters_node.getAttributes(); switch (parameters_node.getNodeName().toLowerCase()) { case "param": { parameters.put(parseString(attrs, "name"), parseString(attrs, "value")); break; } case "skill": { parameters.put(parseString(attrs, "name"), new SkillHolder(parseInteger(attrs, "id"), parseInteger(attrs, "level"))); break; } case "minions": { final List minions = new ArrayList<>(1); for (Node minions_node = parameters_node.getFirstChild(); minions_node != null; minions_node = minions_node.getNextSibling()) { if (minions_node.getNodeName().equalsIgnoreCase("npc")) { attrs = minions_node.getAttributes(); minions.add(new MinionHolder(parseInteger(attrs, "id"), parseInteger(attrs, "count"), parseInteger(attrs, "respawnTime"), parseInteger(attrs, "weightPoint"))); } } if (!minions.isEmpty()) { parameters.put(parseString(parameters_node.getAttributes(), "name"), minions); } break; } } } break; } case "race": case "sex": set.set(npc_node.getNodeName(), npc_node.getTextContent().toUpperCase()); break; case "equipment": { set.set("chestId", parseInteger(attrs, "chest")); set.set("rhandId", parseInteger(attrs, "rhand")); set.set("lhandId", parseInteger(attrs, "lhand")); set.set("weaponEnchant", parseInteger(attrs, "weaponEnchant")); break; } case "acquire": { set.set("expRate", parseDouble(attrs, "expRate")); set.set("sp", parseDouble(attrs, "sp")); set.set("raidPoints", parseDouble(attrs, "raidPoints")); break; } case "stats": { set.set("baseSTR", parseInteger(attrs, "str")); set.set("baseINT", parseInteger(attrs, "int")); set.set("baseDEX", parseInteger(attrs, "dex")); set.set("baseWIT", parseInteger(attrs, "wit")); set.set("baseCON", parseInteger(attrs, "con")); set.set("baseMEN", parseInteger(attrs, "men")); for (Node stats_node = npc_node.getFirstChild(); stats_node != null; stats_node = stats_node.getNextSibling()) { attrs = stats_node.getAttributes(); switch (stats_node.getNodeName().toLowerCase()) { case "vitals": { set.set("baseHpMax", parseDouble(attrs, "hp")); set.set("baseHpReg", parseDouble(attrs, "hpRegen")); set.set("baseMpMax", parseDouble(attrs, "mp")); set.set("baseMpReg", parseDouble(attrs, "mpRegen")); break; } case "attack": { set.set("basePAtk", parseDouble(attrs, "physical")); set.set("baseMAtk", parseDouble(attrs, "magical")); set.set("baseRndDam", parseInteger(attrs, "random")); set.set("baseCritRate", parseInteger(attrs, "critical")); set.set("accuracy", parseDouble(attrs, "accuracy"));// TODO: Implement me set.set("basePAtkSpd", parseInteger(attrs, "attackSpeed")); set.set("reuseDelay", parseInteger(attrs, "reuseDelay"));// TODO: Implement me set.set("baseAtkType", parseString(attrs, "type")); set.set("baseAtkRange", parseInteger(attrs, "range")); set.set("distance", parseInteger(attrs, "distance"));// TODO: Implement me set.set("width", parseInteger(attrs, "width"));// TODO: Implement me break; } case "defence": { set.set("basePDef", parseDouble(attrs, "physical")); set.set("baseMDef", parseDouble(attrs, "magical")); set.set("evasion", parseInteger(attrs, "evasion"));// TODO: Implement me set.set("baseShldDef", parseInteger(attrs, "shield")); set.set("baseShldRate", parseInteger(attrs, "shieldRate")); break; } case "attribute": { for (Node attribute_node = stats_node.getFirstChild(); attribute_node != null; attribute_node = attribute_node.getNextSibling()) { attrs = attribute_node.getAttributes(); switch (attribute_node.getNodeName().toLowerCase()) { case "attack": { String attackAttributeType = parseString(attrs, "type"); switch (attackAttributeType.toUpperCase()) { case "FIRE": set.set("baseFire", parseInteger(attrs, "value")); break; case "WATER": set.set("baseWater", parseInteger(attrs, "value")); break; case "WIND": set.set("baseWind", parseInteger(attrs, "value")); break; case "EARTH": set.set("baseEarth", parseInteger(attrs, "value")); break; case "DARK": set.set("baseDark", parseInteger(attrs, "value")); break; case "HOLY": set.set("baseHoly", parseInteger(attrs, "value")); break; } break; } case "defence": { set.set("baseFireRes", parseInteger(attrs, "fire")); set.set("baseWaterRes", parseInteger(attrs, "water")); set.set("baseWindRes", parseInteger(attrs, "wind")); set.set("baseEarthRes", parseInteger(attrs, "earth")); set.set("baseHolyRes", parseInteger(attrs, "holy")); set.set("baseDarkRes", parseInteger(attrs, "dark")); set.set("baseElementRes", parseInteger(attrs, "default")); break; } } } break; } case "speed": { for (Node speed_node = stats_node.getFirstChild(); speed_node != null; speed_node = speed_node.getNextSibling()) { attrs = speed_node.getAttributes(); switch (speed_node.getNodeName().toLowerCase()) { case "walk": { set.set("baseWalkSpd", parseDouble(attrs, "ground")); set.set("baseSwimWalkSpd", parseDouble(attrs, "swim")); set.set("baseFlyWalkSpd", parseDouble(attrs, "fly")); break; } case "run": { set.set("baseRunSpd", parseDouble(attrs, "ground")); set.set("baseSwimRunSpd", parseDouble(attrs, "swim")); set.set("baseFlyRunSpd", parseDouble(attrs, "fly")); break; } } } break; } case "hit_time": set.set("hit_time", npc_node.getTextContent());// TODO: Implement me default 600 (value in ms) break; } } break; } case "status": { set.set("unique", parseBoolean(attrs, "unique")); set.set("attackable", parseBoolean(attrs, "attackable")); set.set("targetable", parseBoolean(attrs, "targetable")); set.set("undying", parseBoolean(attrs, "undying")); set.set("showName", parseBoolean(attrs, "showName")); set.set("flying", parseBoolean(attrs, "flying")); set.set("canMove", parseBoolean(attrs, "canMove")); set.set("noSleepMode", parseBoolean(attrs, "noSleepMode")); set.set("passableDoor", parseBoolean(attrs, "passableDoor")); set.set("hasSummoner", parseBoolean(attrs, "hasSummoner")); set.set("canBeSown", parseBoolean(attrs, "canBeSown")); break; } case "skill_list": { skills = new HashMap<>(); for (Node skill_list_node = npc_node.getFirstChild(); skill_list_node != null; skill_list_node = skill_list_node.getNextSibling()) { if ("skill".equalsIgnoreCase(skill_list_node.getNodeName())) { attrs = skill_list_node.getAttributes(); final int skillId = parseInteger(attrs, "id"); final int skillLevel = parseInteger(attrs, "level"); final Skill skill = SkillData.getInstance().getSkill(skillId, skillLevel); if (skill != null) { skills.put(skill.getId(), skill); } else { LOGGER.warn("[{}] skill not found. NPC ID: {} Skill ID: {} Skill Level: {}!", f.getName(), npcId, skillId, skillLevel); } } } break; } case "shots": { set.set("soulShot", parseInteger(attrs, "soul")); set.set("spiritShot", parseInteger(attrs, "spirit")); set.set("shotShotChance", parseInteger(attrs, "shotChance")); set.set("spiritShotChance", parseInteger(attrs, "spiritChance")); break; } case "corpse_time": set.set("corpseTime", npc_node.getTextContent()); break; case "ex_crt_effect": set.set("ex_crt_effect", npc_node.getTextContent()); // TODO: Implement me default ? type boolean break; case "s_npc_prop_hp_rate": set.set("s_npc_prop_hp_rate", npc_node.getTextContent()); // TODO: Implement me default 1 type double break; case "ai": { set.set("aiType", parseString(attrs, "type")); set.set("aggroRange", parseInteger(attrs, "aggroRange")); set.set("clanHelpRange", parseInteger(attrs, "clanHelpRange")); set.set("dodge", parseInteger(attrs, "dodge")); set.set("isChaos", parseBoolean(attrs, "isChaos")); set.set("isAggressive", parseBoolean(attrs, "isAggressive")); for (Node ai_node = npc_node.getFirstChild(); ai_node != null; ai_node = ai_node.getNextSibling()) { attrs = ai_node.getAttributes(); switch (ai_node.getNodeName().toLowerCase()) { case "skill": { set.set("minSkillChance", parseInteger(attrs, "minChance")); set.set("maxSkillChance", parseInteger(attrs, "maxChance")); set.set("primarySkillId", parseInteger(attrs, "primaryId")); set.set("shortRangeSkillId", parseInteger(attrs, "shortRangeId")); set.set("shortRangeSkillChance", parseInteger(attrs, "shortRangeChance")); set.set("longRangeSkillId", parseInteger(attrs, "longRangeId")); set.set("longRangeSkillChance", parseInteger(attrs, "longRangeChance")); break; } case "clan_list": { for (Node clan_list_node = ai_node.getFirstChild(); clan_list_node != null; clan_list_node = clan_list_node.getNextSibling()) { attrs = clan_list_node.getAttributes(); switch (clan_list_node.getNodeName().toLowerCase()) { case "clan": { if (clans == null) { clans = new HashSet<>(1); } clans.add(getOrCreateClanId(clan_list_node.getTextContent())); break; } case "ignore_npc_id": { if (ignoreClanNpcIds == null) { ignoreClanNpcIds = new HashSet<>(1); } ignoreClanNpcIds.add(Integer.parseInt(clan_list_node.getTextContent())); break; } } } break; } } } break; } case "drop_lists": { for (Node drop_lists_node = npc_node.getFirstChild(); drop_lists_node != null; drop_lists_node = drop_lists_node.getNextSibling()) { DropListScope dropListScope = null; try { dropListScope = Enum.valueOf(DropListScope.class, drop_lists_node.getNodeName().toUpperCase()); } catch (Exception e) { } if (dropListScope != null) { if (dropLists == null) { dropLists = new EnumMap<>(DropListScope.class); } List dropList = new ArrayList<>(); parseDropList(f, drop_lists_node, dropListScope, dropList); dropLists.put(dropListScope, Collections.unmodifiableList(dropList)); } } break; } case "collision": { for (Node collision_node = npc_node.getFirstChild(); collision_node != null; collision_node = collision_node.getNextSibling()) { attrs = collision_node.getAttributes(); switch (collision_node.getNodeName().toLowerCase()) { case "radius": { set.set("collision_radius", parseDouble(attrs, "normal")); set.set("collisionRadiusGrown", parseDouble(attrs, "grown")); break; } case "height": { set.set("collision_height", parseDouble(attrs, "normal")); set.set("collisionHeightGrown", parseDouble(attrs, "grown")); break; } } } break; } } } L2NpcTemplate template = _npcs.get(npcId); if (template == null) { template = new L2NpcTemplate(set); _npcs.put(template.getId(), template); } else { template.set(set); } if (_minionData._tempMinions.containsKey(npcId)) { if (parameters == null) { parameters = new HashMap<>(); } parameters.putIfAbsent("Privates", _minionData._tempMinions.get(npcId)); } if (parameters != null) { // Using unmodifiable map parameters of template are not meant to be changed at runtime. template.setParameters(new StatsSet(Collections.unmodifiableMap(parameters))); } else { template.setParameters(StatsSet.EMPTY_STATSET); } if (skills != null) { Map> aiSkillLists = null; for (Skill skill : skills.values()) { if (!skill.isPassive()) { if (aiSkillLists == null) { aiSkillLists = new EnumMap<>(AISkillScope.class); } List aiSkillScopes = new ArrayList<>(); final AISkillScope shortOrLongRangeScope = skill.getCastRange() <= 150 ? AISkillScope.SHORT_RANGE : AISkillScope.SHORT_RANGE; if (skill.isSuicideAttack()) { aiSkillScopes.add(AISkillScope.SUICIDE); } else { aiSkillScopes.add(AISkillScope.GENERAL); if (skill.isContinuous()) { if (!skill.isDebuff()) { aiSkillScopes.add(AISkillScope.BUFF); } else { aiSkillScopes.add(AISkillScope.DEBUFF); aiSkillScopes.add(AISkillScope.COT); aiSkillScopes.add(shortOrLongRangeScope); } } else { if (skill.hasEffectType(L2EffectType.DISPEL, L2EffectType.DISPEL_BY_SLOT)) { aiSkillScopes.add(AISkillScope.NEGATIVE); aiSkillScopes.add(shortOrLongRangeScope); } else if (skill.hasEffectType(L2EffectType.HEAL)) { aiSkillScopes.add(AISkillScope.HEAL); } else if (skill.hasEffectType(L2EffectType.PHYSICAL_ATTACK, L2EffectType.PHYSICAL_ATTACK_HP_LINK, L2EffectType.MAGICAL_ATTACK, L2EffectType.DEATH_LINK, L2EffectType.HP_DRAIN)) { aiSkillScopes.add(AISkillScope.ATTACK); aiSkillScopes.add(AISkillScope.UNIVERSAL); aiSkillScopes.add(shortOrLongRangeScope); } else if (skill.hasEffectType(L2EffectType.SLEEP)) { aiSkillScopes.add(AISkillScope.IMMOBILIZE); } else if (skill.hasEffectType(L2EffectType.STUN, L2EffectType.ROOT)) { aiSkillScopes.add(AISkillScope.IMMOBILIZE); aiSkillScopes.add(shortOrLongRangeScope); } else if (skill.hasEffectType(L2EffectType.MUTE, L2EffectType.FEAR)) { aiSkillScopes.add(AISkillScope.COT); aiSkillScopes.add(shortOrLongRangeScope); } else if (skill.hasEffectType(L2EffectType.PARALYZE)) { aiSkillScopes.add(AISkillScope.IMMOBILIZE); aiSkillScopes.add(shortOrLongRangeScope); } else if (skill.hasEffectType(L2EffectType.DMG_OVER_TIME, L2EffectType.DMG_OVER_TIME_PERCENT)) { aiSkillScopes.add(shortOrLongRangeScope); } else if (skill.hasEffectType(L2EffectType.RESURRECTION)) { aiSkillScopes.add(AISkillScope.RES); } else { aiSkillScopes.add(AISkillScope.UNIVERSAL); } } } for (AISkillScope aiSkillScope : aiSkillScopes) { List aiSkills = aiSkillLists.get(aiSkillScope); if (aiSkills == null) { aiSkills = new ArrayList<>(); aiSkillLists.put(aiSkillScope, aiSkills); } aiSkills.add(skill); } } } template.setSkills(skills); template.setAISkillLists(aiSkillLists); } else { template.setSkills(null); template.setAISkillLists(null); } template.setClans(clans); template.setIgnoreClanNpcIds(ignoreClanNpcIds); template.setDropLists(dropLists); } } } } } private void parseDropList(File f, Node drop_list_node, DropListScope dropListScope, List drops) { for (Node drop_node = drop_list_node.getFirstChild(); drop_node != null; drop_node = drop_node.getNextSibling()) { NamedNodeMap attrs = drop_node.getAttributes(); switch (drop_node.getNodeName().toLowerCase()) { case "group": { GroupedGeneralDropItem dropItem = dropListScope.newGroupedDropItem(parseDouble(attrs, "chance")); List groupedDropList = new ArrayList<>(2); for (Node group_node = drop_node.getFirstChild(); group_node != null; group_node = group_node.getNextSibling()) { parseDropListItem(group_node, dropListScope, groupedDropList); } List items = new ArrayList<>(groupedDropList.size()); for (IDropItem item : groupedDropList) { if (item instanceof GeneralDropItem) { items.add((GeneralDropItem) item); } else { LOGGER.warn("[{}] grouped general drop item supports only general drop item.", f); } } dropItem.setItems(items); drops.add(dropItem); break; } default: { parseDropListItem(drop_node, dropListScope, drops); break; } } } } private void parseDropListItem(Node drop_list_item, DropListScope dropListScope, List drops) { NamedNodeMap attrs = drop_list_item.getAttributes(); switch (drop_list_item.getNodeName().toLowerCase()) { case "item": { final IDropItem dropItem = dropListScope.newDropItem(parseInteger(attrs, "id"), parseLong(attrs, "min"), parseLong(attrs, "max"), parseDouble(attrs, "chance")); if (dropItem != null) { drops.add(dropItem); } break; } } } /** * Gets or creates a clan id if it doesnt exists. * @param clanName the clan name to get or create its id * @return the clan id for the given clan name */ private int getOrCreateClanId(String clanName) { Integer id = _clans.get(clanName.toUpperCase()); if (id == null) { id = _clans.size(); _clans.put(clanName.toUpperCase(), id); } return id; } /** * Gets the clan id * @param clanName the clan name to get its id * @return the clan id for the given clan name if it exists, -1 otherwise */ public int getClanId(String clanName) { Integer id = _clans.get(clanName.toUpperCase()); return id != null ? id : -1; } /** * Gets the template. * @param id the template Id to get. * @return the template for the given id. */ public L2NpcTemplate getTemplate(int id) { return _npcs.get(id); } /** * Gets the template by name. * @param name of the template to get. * @return the template for the given name. */ public L2NpcTemplate getTemplateByName(String name) { for (L2NpcTemplate npcTemplate : _npcs.values()) { if (npcTemplate.getName().equalsIgnoreCase(name)) { return npcTemplate; } } return null; } /** * Gets all templates matching the filter. * @param filter * @return the template list for the given filter */ public List getTemplates(Predicate filter) { //@formatter:off return _npcs.values().stream() .filter(filter) .collect(Collectors.toList()); //@formatter:on } /** * Gets the all of level. * @param lvls of all the templates to get. * @return the template list for the given level. */ public List getAllOfLevel(int... lvls) { return getTemplates(template -> Util.contains(lvls, template.getLevel())); } /** * Gets the all monsters of level. * @param lvls of all the monster templates to get. * @return the template list for the given level. */ public List getAllMonstersOfLevel(int... lvls) { return getTemplates(template -> Util.contains(lvls, template.getLevel()) && template.isType("L2Monster")); } /** * Gets the all npc starting with. * @param text of all the NPC templates which its name start with. * @return the template list for the given letter. */ public List getAllNpcStartingWith(String text) { return getTemplates(template -> template.isType("L2Npc") && template.getName().startsWith(text)); } /** * Gets the all npc of class type. * @param classTypes of all the templates to get. * @return the template list for the given class type. */ public List getAllNpcOfClassType(String... classTypes) { return getTemplates(template -> Util.contains(classTypes, template.getType(), true)); } public void loadNpcsSkillLearn() { _npcs.values().forEach(template -> { final List teachInfo = SkillLearnData.getInstance().getSkillLearnData(template.getId()); if (teachInfo != null) { template.addTeachInfo(teachInfo); } }); } /** * This class handles minions from Spawn System
* Once Spawn System gets reworked delete this class
* @author Zealar */ private final class MinionData implements IXmlReader { public final Map> _tempMinions = new HashMap<>(); protected MinionData() { load(); } @Override public void load() { _tempMinions.clear(); parseDatapackFile("data/minionData.xml"); LOGGER.info("{}: Loaded {} minions data.", getClass().getSimpleName(), _tempMinions.size()); } @Override public void parseDocument(Document doc) { for (Node node = doc.getFirstChild(); node != null; node = node.getNextSibling()) { if ("list".equals(node.getNodeName())) { for (Node list_node = node.getFirstChild(); list_node != null; list_node = list_node.getNextSibling()) { if ("npc".equals(list_node.getNodeName())) { final List minions = new ArrayList<>(1); NamedNodeMap attrs = list_node.getAttributes(); int id = parseInteger(attrs, "id"); for (Node npc_node = list_node.getFirstChild(); npc_node != null; npc_node = npc_node.getNextSibling()) { if ("minion".equals(npc_node.getNodeName())) { attrs = npc_node.getAttributes(); minions.add(new MinionHolder(parseInteger(attrs, "id"), parseInteger(attrs, "count"), parseInteger(attrs, "respawnTime"), 0)); } } _tempMinions.put(id, minions); } } } } } } /** * Gets the single instance of NpcData. * @return single instance of NpcData */ public static NpcData getInstance() { return SingletonHolder._instance; } private static class SingletonHolder { protected static final NpcData _instance = new NpcData(); } }