/*
* This file is part of the L2J Mobius project.
*
* This program 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.
*
* This program 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.l2jmobius.gameserver.instancemanager;
import java.io.File;
import java.lang.reflect.Constructor;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.Statement;
import java.time.DayOfWeek;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.w3c.dom.Document;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import com.l2jmobius.Config;
import com.l2jmobius.commons.database.DatabaseFactory;
import com.l2jmobius.commons.util.IGameXmlReader;
import com.l2jmobius.commons.util.IXmlReader;
import com.l2jmobius.gameserver.data.xml.impl.DoorData;
import com.l2jmobius.gameserver.data.xml.impl.SpawnsData;
import com.l2jmobius.gameserver.enums.InstanceReenterType;
import com.l2jmobius.gameserver.enums.InstanceRemoveBuffType;
import com.l2jmobius.gameserver.enums.InstanceTeleportType;
import com.l2jmobius.gameserver.model.Location;
import com.l2jmobius.gameserver.model.StatsSet;
import com.l2jmobius.gameserver.model.actor.instance.L2PcInstance;
import com.l2jmobius.gameserver.model.actor.templates.L2DoorTemplate;
import com.l2jmobius.gameserver.model.holders.InstanceReenterTimeHolder;
import com.l2jmobius.gameserver.model.instancezone.Instance;
import com.l2jmobius.gameserver.model.instancezone.InstanceTemplate;
import com.l2jmobius.gameserver.model.instancezone.conditions.Condition;
import com.l2jmobius.gameserver.model.spawns.SpawnTemplate;
/**
* Instance manager.
* @author evill33t, GodKratos, malyelfik
*/
public final class InstanceManager implements IGameXmlReader
{
private static final Logger LOGGER = Logger.getLogger(InstanceManager.class.getName());
// Database query
private static final String DELETE_INSTANCE_TIME = "DELETE FROM character_instance_time WHERE charId=? AND instanceId=?";
// Client instance names
private final Map _instanceNames = new HashMap<>();
// Instance templates holder
private final Map _instanceTemplates = new HashMap<>();
// Created instance worlds
private int _currentInstanceId = 0;
private final Map _instanceWorlds = new ConcurrentHashMap<>();
// Player reenter times
private final Map> _playerInstanceTimes = new ConcurrentHashMap<>();
protected InstanceManager()
{
load();
}
// --------------------------------------------------------------------
// Instance data loader
// --------------------------------------------------------------------
@Override
public void load()
{
// Load instance names
_instanceNames.clear();
parseDatapackFile("data/InstanceNames.xml");
LOGGER.info(getClass().getSimpleName() + ": Loaded " + _instanceNames.size() + " instance names.");
// Load instance templates
_instanceTemplates.clear();
parseDatapackDirectory("data/instances", true);
LOGGER.info(getClass().getSimpleName() + ": Loaded " + _instanceTemplates.size() + " instance templates.");
// Load player's reenter data
_playerInstanceTimes.clear();
restoreInstanceTimes();
LOGGER.info(getClass().getSimpleName() + ": Loaded instance reenter times for " + _playerInstanceTimes.size() + " players.");
}
@Override
public void parseDocument(Document doc, File f)
{
forEach(doc, IXmlReader::isNode, listNode ->
{
switch (listNode.getNodeName())
{
case "list":
{
parseInstanceName(listNode);
break;
}
case "instance":
{
parseInstanceTemplate(listNode, f);
break;
}
}
});
}
/**
* Read instance names from XML file.
* @param n starting XML tag
*/
private void parseInstanceName(Node n)
{
forEach(n, "instance", instanceNode ->
{
final NamedNodeMap attrs = instanceNode.getAttributes();
_instanceNames.put(parseInteger(attrs, "id"), parseString(attrs, "name"));
});
}
/**
* Parse instance template from XML file.
* @param instanceNode start XML tag
* @param file currently parsed file
*/
private void parseInstanceTemplate(Node instanceNode, File file)
{
// Parse "instance" node
final int id = parseInteger(instanceNode.getAttributes(), "id");
if (_instanceTemplates.containsKey(id))
{
LOGGER.warning(getClass().getSimpleName() + ": Instance template with ID " + id + " already exists");
return;
}
final InstanceTemplate template = new InstanceTemplate(new StatsSet(parseAttributes(instanceNode)));
// Update name if wasn't provided
if (template.getName() == null)
{
template.setName(_instanceNames.get(id));
}
// Parse "instance" node children
forEach(instanceNode, IXmlReader::isNode, innerNode ->
{
switch (innerNode.getNodeName())
{
case "time":
{
final NamedNodeMap attrs = innerNode.getAttributes();
template.setDuration(parseInteger(attrs, "duration", -1));
template.setEmptyDestroyTime(parseInteger(attrs, "empty", -1));
template.setEjectTime(parseInteger(attrs, "eject", -1));
break;
}
case "misc":
{
final NamedNodeMap attrs = innerNode.getAttributes();
template.allowPlayerSummon(parseBoolean(attrs, "allowPlayerSummon", false));
template.setIsPvP(parseBoolean(attrs, "isPvP", false));
break;
}
case "rates":
{
final NamedNodeMap attrs = innerNode.getAttributes();
template.setExpRate(parseFloat(attrs, "exp", Config.RATE_INSTANCE_XP));
template.setSPRate(parseFloat(attrs, "sp", Config.RATE_INSTANCE_SP));
template.setExpPartyRate(parseFloat(attrs, "partyExp", Config.RATE_INSTANCE_PARTY_XP));
template.setSPPartyRate(parseFloat(attrs, "partySp", Config.RATE_INSTANCE_PARTY_SP));
break;
}
case "locations":
{
forEach(innerNode, IXmlReader::isNode, locationsNode ->
{
switch (locationsNode.getNodeName())
{
case "enter":
{
final InstanceTeleportType type = parseEnum(locationsNode.getAttributes(), InstanceTeleportType.class, "type");
final List locations = new ArrayList<>();
forEach(locationsNode, "location", locationNode -> locations.add(parseLocation(locationNode)));
template.setEnterLocation(type, locations);
break;
}
case "exit":
{
final InstanceTeleportType type = parseEnum(locationsNode.getAttributes(), InstanceTeleportType.class, "type");
if (type.equals(InstanceTeleportType.ORIGIN))
{
template.setExitLocation(type, null);
}
else
{
final List locations = new ArrayList<>();
forEach(locationsNode, "location", locationNode -> locations.add(parseLocation(locationNode)));
if (locations.isEmpty())
{
LOGGER.warning(getClass().getSimpleName() + ": Missing exit location data for instance " + template.getName() + " (" + template.getId() + ")!");
}
else
{
template.setExitLocation(type, locations);
}
}
break;
}
}
});
break;
}
case "spawnlist":
{
final List spawns = new ArrayList<>();
SpawnsData.getInstance().parseSpawn(innerNode, file, spawns);
template.addSpawns(spawns);
break;
}
case "doorlist":
{
for (Node doorNode = innerNode.getFirstChild(); doorNode != null; doorNode = doorNode.getNextSibling())
{
if (doorNode.getNodeName().equals("door"))
{
final StatsSet parsedSet = DoorData.getInstance().parseDoor(doorNode);
final StatsSet mergedSet = new StatsSet();
final int doorId = parsedSet.getInt("id");
final StatsSet templateSet = DoorData.getInstance().getDoorTemplate(doorId);
if (templateSet != null)
{
mergedSet.merge(templateSet);
}
else
{
LOGGER.warning(getClass().getSimpleName() + ": Cannot find template for door: " + doorId + ", instance: " + template.getName() + " (" + template.getId() + ")");
}
mergedSet.merge(parsedSet);
try
{
template.addDoor(doorId, new L2DoorTemplate(mergedSet));
}
catch (Exception e)
{
LOGGER.log(Level.WARNING, getClass().getSimpleName() + ": Cannot initialize template for door: " + doorId + ", instance: " + template.getName() + " (" + template.getId() + ")", e);
}
}
}
break;
}
case "removeBuffs":
{
final InstanceRemoveBuffType removeBuffType = parseEnum(innerNode.getAttributes(), InstanceRemoveBuffType.class, "type");
final List exceptionBuffList = new ArrayList<>();
for (Node e = innerNode.getFirstChild(); e != null; e = e.getNextSibling())
{
if (e.getNodeName().equals("skill"))
{
exceptionBuffList.add(parseInteger(e.getAttributes(), "id"));
}
}
template.setRemoveBuff(removeBuffType, exceptionBuffList);
break;
}
case "reenter":
{
final InstanceReenterType type = parseEnum(innerNode.getAttributes(), InstanceReenterType.class, "apply", InstanceReenterType.NONE);
final List data = new ArrayList<>();
for (Node e = innerNode.getFirstChild(); e != null; e = e.getNextSibling())
{
if (e.getNodeName().equals("reset"))
{
final NamedNodeMap attrs = e.getAttributes();
final int time = parseInteger(attrs, "time", -1);
if (time > 0)
{
data.add(new InstanceReenterTimeHolder(time));
}
else
{
final DayOfWeek day = parseEnum(attrs, DayOfWeek.class, "day");
final int hour = parseInteger(attrs, "hour", -1);
final int minute = parseInteger(attrs, "minute", -1);
data.add(new InstanceReenterTimeHolder(day, hour, minute));
}
}
}
template.setReenterData(type, data);
break;
}
case "parameters":
{
template.setParameters(parseParameters(innerNode));
break;
}
case "conditions":
{
final List conditions = new ArrayList<>();
for (Node conditionNode = innerNode.getFirstChild(); conditionNode != null; conditionNode = conditionNode.getNextSibling())
{
if (conditionNode.getNodeName().equals("condition"))
{
final NamedNodeMap attrs = conditionNode.getAttributes();
final String type = parseString(attrs, "type");
final boolean onlyLeader = parseBoolean(attrs, "onlyLeader", false);
final boolean showMessageAndHtml = parseBoolean(attrs, "showMessageAndHtml", false);
// Load parameters
StatsSet params = null;
for (Node f = conditionNode.getFirstChild(); f != null; f = f.getNextSibling())
{
if (f.getNodeName().equals("param"))
{
if (params == null)
{
params = new StatsSet();
}
params.set(parseString(f.getAttributes(), "name"), parseString(f.getAttributes(), "value"));
}
}
// If none parameters found then set empty StatSet
if (params == null)
{
params = StatsSet.EMPTY_STATSET;
}
// Now when everything is loaded register condition to template
try
{
final Class> clazz = Class.forName("com.l2jmobius.gameserver.model.instancezone.conditions.Condition" + type);
final Constructor> constructor = clazz.getConstructor(InstanceTemplate.class, StatsSet.class, boolean.class, boolean.class);
conditions.add((Condition) constructor.newInstance(template, params, onlyLeader, showMessageAndHtml));
}
catch (Exception ex)
{
LOGGER.warning(getClass().getSimpleName() + ": Unknown condition type " + type + " for instance " + template.getName() + " (" + id + ")!");
}
}
}
template.setConditions(conditions);
break;
}
}
});
// Save template
_instanceTemplates.put(id, template);
}
// --------------------------------------------------------------------
// Instance data loader - END
// --------------------------------------------------------------------
/**
* Create new instance with default template.
* @return newly created default instance.
*/
public Instance createInstance()
{
return new Instance(getNewInstanceId(), new InstanceTemplate(StatsSet.EMPTY_STATSET), null);
}
/**
* Create new instance from given template.
* @param template template used for instance creation
* @param player player who create instance.
* @return newly created instance if success, otherwise {@code null}
*/
public Instance createInstance(InstanceTemplate template, L2PcInstance player)
{
return (template != null) ? new Instance(getNewInstanceId(), template, player) : null;
}
/**
* Create new instance with template defined in datapack.
* @param id template id of instance
* @param player player who create instance
* @return newly created instance if template was found, otherwise {@code null}
*/
public Instance createInstance(int id, L2PcInstance player)
{
if (!_instanceTemplates.containsKey(id))
{
LOGGER.warning(getClass().getSimpleName() + ": Missing template for instance with id " + id + "!");
return null;
}
return new Instance(getNewInstanceId(), _instanceTemplates.get(id), player);
}
/**
* Get instance world with given ID.
* @param instanceId ID of instance
* @return instance itself if found, otherwise {@code null}
*/
public Instance getInstance(int instanceId)
{
return _instanceWorlds.get(instanceId);
}
/**
* Get all active instances.
* @return Collection of all instances
*/
public Collection getInstances()
{
return _instanceWorlds.values();
}
/**
* Get instance world for player.
* @param player player who wants to get instance world
* @param isInside when {@code true} find world where player is currently located, otherwise find world where player can enter
* @return instance if found, otherwise {@code null}
*/
public Instance getPlayerInstance(L2PcInstance player, boolean isInside)
{
return _instanceWorlds.values().stream().filter(i -> (isInside) ? i.containsPlayer(player) : i.isAllowed(player)).findFirst().orElse(null);
}
/**
* Get ID for newly created instance.
* @return instance id
*/
private synchronized int getNewInstanceId()
{
do
{
if (_currentInstanceId == Integer.MAX_VALUE)
{
if (Config.DEBUG_INSTANCES)
{
LOGGER.info(getClass().getSimpleName() + ": Instance id owerflow, starting from zero.");
}
_currentInstanceId = 0;
}
_currentInstanceId++;
}
while (_instanceWorlds.containsKey(_currentInstanceId));
return _currentInstanceId;
}
/**
* Register instance world.
* @param instance instance which should be registered
*/
public void register(Instance instance)
{
final int instanceId = instance.getId();
if (!_instanceWorlds.containsKey(instanceId))
{
_instanceWorlds.put(instanceId, instance);
}
}
/**
* Unregister instance world.
* To remove instance world properly use {@link Instance#destroy()}.
* @param instanceId ID of instance to unregister
*/
public void unregister(int instanceId)
{
if (_instanceWorlds.containsKey(instanceId))
{
_instanceWorlds.remove(instanceId);
}
}
/**
* Get instance name from file "InstanceNames.xml"
* @param templateId template ID of instance
* @return name of instance if found, otherwise {@code null}
*/
public String getInstanceName(int templateId)
{
return _instanceNames.get(templateId);
}
/**
* Restore instance reenter data for all players.
*/
private void restoreInstanceTimes()
{
try (Connection con = DatabaseFactory.getInstance().getConnection();
Statement ps = con.createStatement();
ResultSet rs = ps.executeQuery("SELECT * FROM character_instance_time ORDER BY charId"))
{
while (rs.next())
{
// Check if instance penalty passed
final long time = rs.getLong("time");
if (time > System.currentTimeMillis())
{
// Load params
final int charId = rs.getInt("charId");
final int instanceId = rs.getInt("instanceId");
// Set penalty
setReenterPenalty(charId, instanceId, time);
}
}
}
catch (Exception e)
{
LOGGER.log(Level.WARNING, getClass().getSimpleName() + ": Cannot restore players instance reenter data: ", e);
}
}
/**
* Get all instance re-enter times for specified player.
* This method also removes the penalties that have already expired.
* @param player instance of player who wants to get re-enter data
* @return map in form templateId, penaltyEndTime
*/
public Map getAllInstanceTimes(L2PcInstance player)
{
// When player don't have any instance penalty
final Map instanceTimes = _playerInstanceTimes.get(player.getObjectId());
if ((instanceTimes == null) || instanceTimes.isEmpty())
{
return Collections.emptyMap();
}
// Find passed penalty
final List invalidPenalty = new ArrayList<>(instanceTimes.size());
for (Entry entry : instanceTimes.entrySet())
{
if (entry.getValue() <= System.currentTimeMillis())
{
invalidPenalty.add(entry.getKey());
}
}
// Remove them
if (!invalidPenalty.isEmpty())
{
try (Connection con = DatabaseFactory.getInstance().getConnection();
PreparedStatement ps = con.prepareStatement(DELETE_INSTANCE_TIME))
{
for (Integer id : invalidPenalty)
{
ps.setInt(1, player.getObjectId());
ps.setInt(2, id);
ps.addBatch();
}
ps.executeBatch();
invalidPenalty.forEach(instanceTimes::remove);
}
catch (Exception e)
{
LOGGER.log(Level.WARNING, getClass().getSimpleName() + ": Cannot delete instance character reenter data: ", e);
}
}
return instanceTimes;
}
/**
* Set re-enter penalty for specified player.
* This method store penalty into memory only. Use {@link Instance#setReenterTime} to set instance penalty properly.
* @param objectId object ID of player
* @param id instance template id
* @param time penalty time
*/
public void setReenterPenalty(int objectId, int id, long time)
{
_playerInstanceTimes.computeIfAbsent(objectId, k -> new ConcurrentHashMap<>()).put(id, time);
}
/**
* Get re-enter time to instance (by template ID) for player.
* This method also removes penalty if expired.
* @param player player who wants to get re-enter time
* @param id template ID of instance
* @return penalty end time if penalty is found, otherwise -1
*/
public long getInstanceTime(L2PcInstance player, int id)
{
// Check if exists reenter data for player
final Map playerData = _playerInstanceTimes.get(player.getObjectId());
if ((playerData == null) || !playerData.containsKey(id))
{
return -1;
}
// If reenter time is higher then current, delete it
final long time = playerData.get(id);
if (time <= System.currentTimeMillis())
{
deleteInstanceTime(player, id);
return -1;
}
return time;
}
/**
* Remove re-enter penalty for specified instance from player.
* @param player player who wants to delete penalty
* @param id template id of instance world
*/
public void deleteInstanceTime(L2PcInstance player, int id)
{
try (Connection con = DatabaseFactory.getInstance().getConnection();
PreparedStatement ps = con.prepareStatement(DELETE_INSTANCE_TIME))
{
ps.setInt(1, player.getObjectId());
ps.setInt(2, id);
ps.execute();
_playerInstanceTimes.get(player.getObjectId()).remove(id);
}
catch (Exception e)
{
LOGGER.log(Level.WARNING, getClass().getSimpleName() + ": Could not delete character instance reenter data: ", e);
}
}
/**
* Get instance template by template ID.
* @param id template id of instance
* @return instance template if found, otherwise {@code null}
*/
public InstanceTemplate getInstanceTemplate(int id)
{
return _instanceTemplates.get(id);
}
/**
* Get all instances template.
* @return Collection of all instance templates
*/
public Collection getInstanceTemplates()
{
return _instanceTemplates.values();
}
/**
* Get count of created instance worlds with same template ID.
* @param templateId template id of instance
* @return count of created instances
*/
public long getWorldCount(int templateId)
{
return _instanceWorlds.values().stream().filter(i -> i.getTemplateId() == templateId).count();
}
/**
* Gets the single instance of {@code InstanceManager}.
* @return single instance of {@code InstanceManager}
*/
public static InstanceManager getInstance()
{
return SingletonHolder._instance;
}
private static class SingletonHolder
{
protected static final InstanceManager _instance = new InstanceManager();
}
}