Dropped SQL spawnlist.
This commit is contained in:
parent
aa520ca56a
commit
3b22c6321f
@ -1,15 +0,0 @@
|
||||
CREATE TABLE IF NOT EXISTS `custom_spawnlist` (
|
||||
`location` varchar(40) NOT NULL DEFAULT '',
|
||||
`count` tinyint(1) unsigned NOT NULL DEFAULT '0',
|
||||
`npc_templateid` mediumint(7) unsigned NOT NULL DEFAULT '0',
|
||||
`locx` mediumint(6) NOT NULL DEFAULT '0',
|
||||
`locy` mediumint(6) NOT NULL DEFAULT '0',
|
||||
`locz` mediumint(6) NOT NULL DEFAULT '0',
|
||||
`randomx` mediumint(6) NOT NULL DEFAULT '0',
|
||||
`randomy` mediumint(6) NOT NULL DEFAULT '0',
|
||||
`heading` mediumint(6) NOT NULL DEFAULT '0',
|
||||
`respawn_delay` mediumint(5) NOT NULL DEFAULT '0',
|
||||
`respawn_random` mediumint(5) NOT NULL DEFAULT '0',
|
||||
`loc_id` int(9) NOT NULL DEFAULT '0',
|
||||
`periodOfDay` tinyint(1) unsigned NOT NULL DEFAULT '0'
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
File diff suppressed because it is too large
Load Diff
@ -754,13 +754,6 @@ SafeEnchantCostMultipiler = 5
|
||||
# Custom Components
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# Default: False
|
||||
CustomSpawnlistTable = True
|
||||
|
||||
# Option to save GM spawn only in the custom table.
|
||||
# Default: False
|
||||
SaveGmSpawnOnCustom = True
|
||||
|
||||
# Default: False
|
||||
CustomNpcData = True
|
||||
|
||||
|
@ -22,7 +22,8 @@
|
||||
<tr><td><table width=270 border=0 bgcolor=131210><tr><td width=100><font color="LEVEL">Race</font></td><td align=right width=170>%race%</td></tr></table></td></tr>
|
||||
<tr><td><table width=270 border=0><tr><td width=100><font color="LEVEL">Territory:</font></td><td align=right width=170>%territory%</td></tr></table></td></tr>
|
||||
<tr><td><table width=270 border=0 bgcolor=131210><tr><td width=100><font color="LEVEL">Spawn type:</font></td><td align=right width=170>%spawntype%</td></tr></table></td></tr>
|
||||
<tr><td><table width=270 border=0><tr><td width=100><font color="LEVEL">Spawn loc:</font></td><td align=right width=170>%spawn%</td></tr></table></td></tr>
|
||||
<tr><td><table width=270 border=0><tr><td width=100><font color="LEVEL">Spawn file:</font></td><td align=right width=170>%spawnfile%</td></tr></table></td></tr>
|
||||
<tr><td><table width=270 border=0 bgcolor=131210><tr><td width=100><font color="LEVEL">Spawn loc:</font></td><td align=right width=170>%spawn%</td></tr></table></td></tr>
|
||||
<tr><td><table width=270 border=0><tr><td width=100><font color="LEVEL">Location:</font></td><td align=right width=170>%loc%</td></tr></table></td></tr>
|
||||
<tr><td><table width=270 border=0 bgcolor=131210><tr><td width=100><font color="LEVEL">Heading:</font></td><td align=right width=170>%heading%</td></tr></table></td></tr>
|
||||
<tr><td><table width=270 border=0><tr><td width=100><font color="LEVEL">Collision Radius:</font></td><td align=right width=170>%collision_radius%</td></tr></table></td></tr>
|
||||
|
@ -279,7 +279,7 @@ public final class NpcLocationInfo extends Quest
|
||||
if (Util.contains(NPCRADAR, npcId))
|
||||
{
|
||||
int x = 0, y = 0, z = 0;
|
||||
final L2Spawn spawn = SpawnTable.getInstance().findAny(npcId);
|
||||
final L2Spawn spawn = SpawnTable.getInstance().getAnySpawn(npcId);
|
||||
if (spawn != null)
|
||||
{
|
||||
x = spawn.getX();
|
||||
|
@ -79,7 +79,7 @@ public final class RaidbossInfo extends Quest
|
||||
|
||||
if (RAIDS.contains(bossId))
|
||||
{
|
||||
final L2Spawn spawn = SpawnTable.getInstance().findAny(bossId);
|
||||
final L2Spawn spawn = SpawnTable.getInstance().getAnySpawn(bossId);
|
||||
if (spawn != null)
|
||||
{
|
||||
final Location loc = spawn.getLocation();
|
||||
|
@ -93,9 +93,9 @@ public class Lindvior extends AbstractNpcAI
|
||||
}
|
||||
case "start":
|
||||
{
|
||||
_lindviorCamera = SpawnTable.getInstance().findAny(LINDVIOR_CAMERA).getLastSpawn();
|
||||
_tomaris = SpawnTable.getInstance().findAny(TOMARIS).getLastSpawn();
|
||||
_artius = SpawnTable.getInstance().findAny(ARTIUS).getLastSpawn();
|
||||
_lindviorCamera = SpawnTable.getInstance().getAnySpawn(LINDVIOR_CAMERA).getLastSpawn();
|
||||
_tomaris = SpawnTable.getInstance().getAnySpawn(TOMARIS).getLastSpawn();
|
||||
_artius = SpawnTable.getInstance().getAnySpawn(ARTIUS).getLastSpawn();
|
||||
startQuestTimer("tomaris_shout1", 1000, _tomaris, null);
|
||||
startQuestTimer("artius_shout", 60000, _artius, null);
|
||||
startQuestTimer("tomaris_shout2", 90000, _tomaris, null);
|
||||
|
@ -19,6 +19,7 @@ package handlers.actionshifthandlers;
|
||||
import java.util.Set;
|
||||
|
||||
import com.l2jmobius.Config;
|
||||
import com.l2jmobius.gameserver.datatables.SpawnTable;
|
||||
import com.l2jmobius.gameserver.enums.InstanceType;
|
||||
import com.l2jmobius.gameserver.handler.IActionShiftHandler;
|
||||
import com.l2jmobius.gameserver.instancemanager.WalkingManager;
|
||||
@ -134,15 +135,19 @@ public class L2NpcActionShift implements IActionShiftHandler
|
||||
}
|
||||
else if (((L2Npc) target).getSpawn().hasRespawnRandom())
|
||||
{
|
||||
html.replace("%resp%", ((L2Npc) target).getSpawn().getRespawnMinDelay() / 1000 + "-" + (((L2Npc) target).getSpawn().getRespawnMaxDelay() / 1000) + " sec");
|
||||
html.replace("%resp%", (((L2Npc) target).getSpawn().getRespawnMinDelay() / 1000) + "-" + (((L2Npc) target).getSpawn().getRespawnMaxDelay() / 1000) + " sec");
|
||||
}
|
||||
else
|
||||
{
|
||||
html.replace("%resp%", ((L2Npc) target).getSpawn().getRespawnMinDelay() / 1000 + " sec");
|
||||
html.replace("%resp%", (((L2Npc) target).getSpawn().getRespawnMinDelay() / 1000) + " sec");
|
||||
}
|
||||
|
||||
final String spawnFile = SpawnTable.getInstance().getSpawnFile(((L2Npc) target).getSpawn().getNpcSpawnTemplateId());
|
||||
html.replace("%spawnfile%", spawnFile.substring(spawnFile.lastIndexOf("\\") + 1));
|
||||
}
|
||||
else
|
||||
{
|
||||
html.replace("%spawnfile%", "<font color=FF0000>--</font>");
|
||||
html.replace("%territory%", "<font color=FF0000>--</font>");
|
||||
html.replace("%spawntype%", "<font color=FF0000>--</font>");
|
||||
html.replace("%spawn%", "<font color=FF0000>null</font>");
|
||||
|
@ -23,7 +23,6 @@ import java.util.logging.Logger;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import com.l2jmobius.Config;
|
||||
import com.l2jmobius.gameserver.SevenSigns;
|
||||
import com.l2jmobius.gameserver.data.xml.impl.AdminData;
|
||||
import com.l2jmobius.gameserver.data.xml.impl.NpcData;
|
||||
@ -417,10 +416,6 @@ public class AdminSpawn implements IAdminCommandHandler
|
||||
try
|
||||
{
|
||||
final L2Spawn spawn = new L2Spawn(template);
|
||||
if (Config.SAVE_GMSPAWN_ON_CUSTOM)
|
||||
{
|
||||
spawn.setCustom(true);
|
||||
}
|
||||
spawn.setX(target.getX());
|
||||
spawn.setY(target.getY());
|
||||
spawn.setZ(target.getZ());
|
||||
|
@ -23,7 +23,6 @@ import java.util.NoSuchElementException;
|
||||
import java.util.StringTokenizer;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import com.l2jmobius.Config;
|
||||
import com.l2jmobius.commons.database.DatabaseFactory;
|
||||
import com.l2jmobius.gameserver.ai.CtrlIntention;
|
||||
import com.l2jmobius.gameserver.datatables.SpawnTable;
|
||||
@ -545,10 +544,6 @@ public class AdminTeleport implements IAdminCommandHandler
|
||||
try
|
||||
{
|
||||
spawn = new L2Spawn(target.getTemplate().getId());
|
||||
if (Config.SAVE_GMSPAWN_ON_CUSTOM)
|
||||
{
|
||||
spawn.setCustom(true);
|
||||
}
|
||||
spawn.setX(activeChar.getX());
|
||||
spawn.setY(activeChar.getY());
|
||||
spawn.setZ(activeChar.getZ());
|
||||
@ -590,10 +585,6 @@ public class AdminTeleport implements IAdminCommandHandler
|
||||
try
|
||||
{
|
||||
final L2Spawn spawnDat = new L2Spawn(target.getId());
|
||||
if (Config.SAVE_GMSPAWN_ON_CUSTOM)
|
||||
{
|
||||
spawn.setCustom(true);
|
||||
}
|
||||
spawnDat.setX(activeChar.getX());
|
||||
spawnDat.setY(activeChar.getY());
|
||||
spawnDat.setZ(activeChar.getZ());
|
||||
|
@ -286,7 +286,7 @@ public class DropSearchBoard implements IParseBoardHandler
|
||||
case "_bbs_npc_trace":
|
||||
{
|
||||
int npcId = Integer.parseInt(params[1]);
|
||||
L2Spawn spawn = SpawnTable.getInstance().findAny(npcId);
|
||||
L2Spawn spawn = SpawnTable.getInstance().getAnySpawn(npcId);
|
||||
if (spawn == null)
|
||||
{
|
||||
player.sendMessage("Cannot find any spawn. Maybe dropped by a boss or instance monster.");
|
||||
|
@ -267,6 +267,6 @@ public final class Q00604_DaimonTheWhiteEyedPart2 extends Quest
|
||||
|
||||
private static boolean isDaimonSpawned()
|
||||
{
|
||||
return SpawnTable.getInstance().findAny(DAIMON_THE_WHITE_EYED) != null;
|
||||
return SpawnTable.getInstance().getAnySpawn(DAIMON_THE_WHITE_EYED) != null;
|
||||
}
|
||||
}
|
||||
|
@ -280,6 +280,6 @@ public final class Q00625_TheFinestIngredientsPart2 extends Quest
|
||||
|
||||
private static boolean isBumbalumpSpawned()
|
||||
{
|
||||
return SpawnTable.getInstance().findAny(ICICLE_EMPEROR_BUMBALUMP) != null;
|
||||
return SpawnTable.getInstance().getAnySpawn(ICICLE_EMPEROR_BUMBALUMP) != null;
|
||||
}
|
||||
}
|
38201
L2J_Mobius_CT_2.6_HighFive/dist/game/data/spawns/HighFiveSpawns.xml
vendored
Normal file
38201
L2J_Mobius_CT_2.6_HighFive/dist/game/data/spawns/HighFiveSpawns.xml
vendored
Normal file
File diff suppressed because it is too large
Load Diff
@ -1,5 +1,5 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<list enabled="true" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../xsd/spawnlist.xsd">
|
||||
<list enabled="true" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../xsd/spawns.xsd">
|
||||
<spawn name="chapel_guard_spawns">
|
||||
<AIData>
|
||||
<disableRandomWalk>true</disableRandomWalk>
|
||||
|
@ -1,5 +1,5 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<list enabled="true" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../xsd/spawnlist.xsd">
|
||||
<list enabled="true" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../xsd/spawns.xsd">
|
||||
<spawn zone="aden03_tb2417_01">
|
||||
<npc id="18283" respawnDelay="90" count="1" /> <!-- Treasure Chest -->
|
||||
<npc id="21819" respawnDelay="90" count="1" /> <!-- Treasure Chest -->
|
||||
|
@ -1,5 +1,5 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<list enabled="true" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../xsd/spawnlist.xsd">
|
||||
<list enabled="true" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../xsd/spawns.xsd">
|
||||
<!-- oren22_2219_a01 -->
|
||||
<spawn name="smtg_drill_group_01">
|
||||
<AIData>
|
||||
|
@ -1,5 +1,5 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<list enabled="false" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../xsd/spawnlist.xsd">
|
||||
<list enabled="false" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../xsd/spawns.xsd">
|
||||
<!-- gludio15_1621_01m1 -->
|
||||
<spawn>
|
||||
<AIData>
|
||||
|
@ -1,5 +1,5 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<list enabled="true" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../xsd/spawnlist.xsd">
|
||||
<list enabled="true" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../xsd/spawns.xsd">
|
||||
<!-- gludio15_1621_01m1 -->
|
||||
<spawn zone="turek_orc_zone_01">
|
||||
<AIData>
|
||||
|
@ -646,8 +646,6 @@ public final class Config
|
||||
public static boolean JAIL_IS_PVP;
|
||||
public static boolean JAIL_DISABLE_CHAT;
|
||||
public static boolean JAIL_DISABLE_TRANSACTION;
|
||||
public static boolean CUSTOM_SPAWNLIST_TABLE;
|
||||
public static boolean SAVE_GMSPAWN_ON_CUSTOM;
|
||||
public static boolean CUSTOM_NPC_DATA;
|
||||
public static boolean CUSTOM_TELEPORT_TABLE;
|
||||
public static boolean CUSTOM_NPCBUFFER_TABLES;
|
||||
@ -2008,8 +2006,6 @@ public final class Config
|
||||
JAIL_IS_PVP = General.getBoolean("JailIsPvp", false);
|
||||
JAIL_DISABLE_CHAT = General.getBoolean("JailDisableChat", true);
|
||||
JAIL_DISABLE_TRANSACTION = General.getBoolean("JailDisableTransaction", false);
|
||||
CUSTOM_SPAWNLIST_TABLE = General.getBoolean("CustomSpawnlistTable", false);
|
||||
SAVE_GMSPAWN_ON_CUSTOM = General.getBoolean("SaveGmSpawnOnCustom", false);
|
||||
CUSTOM_NPC_DATA = General.getBoolean("CustomNpcData", false);
|
||||
CUSTOM_TELEPORT_TABLE = General.getBoolean("CustomTeleportTable", false);
|
||||
CUSTOM_NPCBUFFER_TABLES = General.getBoolean("CustomNpcBufferTables", false);
|
||||
|
@ -16,14 +16,15 @@
|
||||
*/
|
||||
package com.l2jmobius.gameserver.datatables;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.BufferedWriter;
|
||||
import java.io.File;
|
||||
import java.sql.Connection;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.Statement;
|
||||
import java.io.FileReader;
|
||||
import java.io.FileWriter;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.function.Function;
|
||||
@ -35,50 +36,36 @@ 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.gameserver.data.xml.impl.NpcData;
|
||||
import com.l2jmobius.gameserver.instancemanager.DayNightSpawnManager;
|
||||
import com.l2jmobius.gameserver.instancemanager.ZoneManager;
|
||||
import com.l2jmobius.gameserver.model.L2Spawn;
|
||||
import com.l2jmobius.gameserver.model.L2World;
|
||||
import com.l2jmobius.gameserver.model.StatsSet;
|
||||
import com.l2jmobius.gameserver.model.actor.templates.L2NpcTemplate;
|
||||
|
||||
/**
|
||||
* Spawn data retriever.
|
||||
* @author Zoey76
|
||||
* @author Zoey76, Mobius
|
||||
*/
|
||||
public final class SpawnTable implements IGameXmlReader
|
||||
{
|
||||
private static final Logger LOGGER = Logger.getLogger(SpawnTable.class.getName());
|
||||
// SQL
|
||||
private static final String SELECT_SPAWNS = "SELECT count, npc_templateid, locx, locy, locz, heading, respawn_delay, respawn_random, loc_id, periodOfDay FROM spawnlist";
|
||||
private static final String SELECT_CUSTOM_SPAWNS = "SELECT count, npc_templateid, locx, locy, locz, heading, respawn_delay, respawn_random, loc_id, periodOfDay FROM custom_spawnlist";
|
||||
|
||||
private static final String OTHER_XML_FOLDER = "data/spawns/Others";
|
||||
private static final Map<Integer, Set<L2Spawn>> _spawnTable = new ConcurrentHashMap<>();
|
||||
private static final Map<Integer, String> _spawnTemplates = new HashMap<>();
|
||||
private int _spanwCount = 0;
|
||||
|
||||
private int _xmlSpawnCount = 0;
|
||||
|
||||
/**
|
||||
* Wrapper to load all spawns.
|
||||
*/
|
||||
@Override
|
||||
public void load()
|
||||
{
|
||||
_spawnTemplates.put(0, "None");
|
||||
if (!Config.ALT_DEV_NO_SPAWNS)
|
||||
{
|
||||
fillSpawnTable(false);
|
||||
final int spawnCount = _spawnTable.size();
|
||||
LOGGER.info(getClass().getSimpleName() + ": Loaded " + spawnCount + " npc spawns.");
|
||||
if (Config.CUSTOM_SPAWNLIST_TABLE)
|
||||
{
|
||||
fillSpawnTable(true);
|
||||
LOGGER.info(getClass().getSimpleName() + ": Loaded " + (_spawnTable.size() - spawnCount) + " custom npc spawns.");
|
||||
}
|
||||
|
||||
// Load XML list
|
||||
parseDatapackDirectory("data/spawns", false);
|
||||
LOGGER.info(getClass().getSimpleName() + ": Loaded " + _xmlSpawnCount + " npc spawns from XML.");
|
||||
LOGGER.info(getClass().getSimpleName() + ": Initializing spawns...");
|
||||
parseDatapackDirectory("data/spawns", true);
|
||||
LOGGER.info(getClass().getSimpleName() + ": " + _spanwCount + " spawns have been initialized!");
|
||||
}
|
||||
}
|
||||
|
||||
@ -179,6 +166,13 @@ public final class SpawnTable implements IGameXmlReader
|
||||
{
|
||||
// mandatory
|
||||
final int templateId = parseInteger(attrs, "id");
|
||||
|
||||
// avoid spawning unwanted spawns
|
||||
if (!checkTemplate(templateId))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// coordinates are optional, if territory is specified; mandatory otherwise
|
||||
int x = 0;
|
||||
int y = 0;
|
||||
@ -239,7 +233,9 @@ public final class SpawnTable implements IGameXmlReader
|
||||
}
|
||||
}
|
||||
|
||||
_xmlSpawnCount += addSpawn(spawnInfo, map);
|
||||
spawnInfo.set("fileName", f.getPath());
|
||||
|
||||
_spanwCount += addSpawn(spawnInfo, map);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -248,51 +244,6 @@ public final class SpawnTable implements IGameXmlReader
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves spawn data from database.
|
||||
* @param isCustom if {@code true} the spawns are loaded as custom from custom spawn table
|
||||
* @return the spawn count
|
||||
*/
|
||||
private int fillSpawnTable(boolean isCustom)
|
||||
{
|
||||
int npcSpawnCount = 0;
|
||||
try (Connection con = DatabaseFactory.getInstance().getConnection();
|
||||
Statement s = con.createStatement();
|
||||
ResultSet rs = s.executeQuery(isCustom ? SELECT_CUSTOM_SPAWNS : SELECT_SPAWNS))
|
||||
{
|
||||
while (rs.next())
|
||||
{
|
||||
final StatsSet spawnInfo = new StatsSet();
|
||||
final int npcId = rs.getInt("npc_templateid");
|
||||
|
||||
// Check basic requirements first
|
||||
if (!checkTemplate(npcId))
|
||||
{
|
||||
// Don't spawn
|
||||
continue;
|
||||
}
|
||||
|
||||
spawnInfo.set("npcTemplateid", npcId);
|
||||
spawnInfo.set("count", rs.getInt("count"));
|
||||
spawnInfo.set("x", rs.getInt("locx"));
|
||||
spawnInfo.set("y", rs.getInt("locy"));
|
||||
spawnInfo.set("z", rs.getInt("locz"));
|
||||
spawnInfo.set("heading", rs.getInt("heading"));
|
||||
spawnInfo.set("respawnDelay", rs.getInt("respawn_delay"));
|
||||
spawnInfo.set("respawnRandom", rs.getInt("respawn_random"));
|
||||
spawnInfo.set("locId", rs.getInt("loc_id"));
|
||||
spawnInfo.set("periodOfDay", rs.getInt("periodOfDay"));
|
||||
spawnInfo.set("isCustomSpawn", isCustom);
|
||||
npcSpawnCount += addSpawn(spawnInfo);
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
LOGGER.log(Level.WARNING, getClass().getSimpleName() + ": Spawn could not be initialized: " + e.getMessage(), e);
|
||||
}
|
||||
return npcSpawnCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates NPC spawn
|
||||
* @param spawnInfo StatsSet of spawn parameters
|
||||
@ -315,7 +266,6 @@ public final class SpawnTable implements IGameXmlReader
|
||||
spawnDat.setLocationId(spawnInfo.getInt("locId", 0));
|
||||
final String territoryName = spawnInfo.getString("territoryName", "");
|
||||
final String spawnName = spawnInfo.getString("spawnName", "");
|
||||
spawnDat.setCustom(spawnInfo.getBoolean("isCustomSpawn", false));
|
||||
if (!spawnName.isEmpty())
|
||||
{
|
||||
spawnDat.setName(spawnName);
|
||||
@ -347,6 +297,25 @@ public final class SpawnTable implements IGameXmlReader
|
||||
}
|
||||
}
|
||||
|
||||
final String fileName = spawnInfo.getString("fileName", "None");
|
||||
if (_spawnTemplates.values().contains(fileName))
|
||||
{
|
||||
for (Entry<Integer, String> entry : _spawnTemplates.entrySet())
|
||||
{
|
||||
if (entry.getValue().equals(fileName))
|
||||
{
|
||||
spawnDat.setSpawnTemplateId(entry.getKey());
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
final int newId = _spawnTemplates.size();
|
||||
_spawnTemplates.put(newId, fileName);
|
||||
spawnDat.setSpawnTemplateId(newId);
|
||||
}
|
||||
|
||||
addSpawn(spawnDat);
|
||||
}
|
||||
catch (Exception e)
|
||||
@ -358,16 +327,6 @@ public final class SpawnTable implements IGameXmlReader
|
||||
return ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper for {@link #addSpawn(StatsSet, Map)}.
|
||||
* @param spawnInfo StatsSet of spawn parameters
|
||||
* @return count NPC instances, spawned by this spawn
|
||||
*/
|
||||
private int addSpawn(StatsSet spawnInfo)
|
||||
{
|
||||
return addSpawn(spawnInfo, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the spawn data.
|
||||
* @return the spawn data
|
||||
@ -398,11 +357,11 @@ public final class SpawnTable implements IGameXmlReader
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds a spawn for the given NPC ID.
|
||||
* Gets a spawn for the given NPC ID.
|
||||
* @param npcId the NPC Id
|
||||
* @return a spawn for the given NPC ID or {@code null}
|
||||
*/
|
||||
public L2Spawn findAny(int npcId)
|
||||
public L2Spawn getAnySpawn(int npcId)
|
||||
{
|
||||
return getSpawns(npcId).stream().findFirst().orElse(null);
|
||||
}
|
||||
@ -410,32 +369,93 @@ public final class SpawnTable implements IGameXmlReader
|
||||
/**
|
||||
* Adds a new spawn to the spawn table.
|
||||
* @param spawn the spawn to add
|
||||
* @param storeInDb if {@code true} it'll be saved in the database
|
||||
* @param store if {@code true} it'll be saved in the spawn XML files
|
||||
*/
|
||||
public void addNewSpawn(L2Spawn spawn, boolean storeInDb)
|
||||
public void addNewSpawn(L2Spawn spawn, boolean store)
|
||||
{
|
||||
addSpawn(spawn);
|
||||
|
||||
if (storeInDb)
|
||||
if (store)
|
||||
{
|
||||
final String spawnTable = spawn.isCustom() && Config.CUSTOM_SPAWNLIST_TABLE ? "custom_spawnlist" : "spawnlist";
|
||||
try (Connection con = DatabaseFactory.getInstance().getConnection();
|
||||
PreparedStatement insert = con.prepareStatement("INSERT INTO " + spawnTable + "(count,npc_templateid,locx,locy,locz,heading,respawn_delay,respawn_random,loc_id) values(?,?,?,?,?,?,?,?,?)"))
|
||||
// Create output directory if it doesn't exist
|
||||
final File outputDirectory = new File(OTHER_XML_FOLDER);
|
||||
if (!outputDirectory.exists())
|
||||
{
|
||||
insert.setInt(1, spawn.getAmount());
|
||||
insert.setInt(2, spawn.getId());
|
||||
insert.setInt(3, spawn.getX());
|
||||
insert.setInt(4, spawn.getY());
|
||||
insert.setInt(5, spawn.getZ());
|
||||
insert.setInt(6, spawn.getHeading());
|
||||
insert.setInt(7, spawn.getRespawnDelay() / 1000);
|
||||
insert.setInt(8, spawn.getRespawnMaxDelay() - spawn.getRespawnMinDelay());
|
||||
insert.setInt(9, spawn.getLocationId());
|
||||
insert.execute();
|
||||
boolean result = false;
|
||||
try
|
||||
{
|
||||
outputDirectory.mkdir();
|
||||
result = true;
|
||||
}
|
||||
catch (SecurityException se)
|
||||
{
|
||||
// empty
|
||||
}
|
||||
if (result)
|
||||
{
|
||||
LOGGER.info(getClass().getSimpleName() + ": Created directory: " + OTHER_XML_FOLDER);
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
|
||||
// XML file for spawn
|
||||
final int x = ((spawn.getX() - L2World.MAP_MIN_X) >> 15) + L2World.TILE_X_MIN;
|
||||
final int y = ((spawn.getY() - L2World.MAP_MIN_Y) >> 15) + L2World.TILE_Y_MIN;
|
||||
final File spawnFile = new File(OTHER_XML_FOLDER + "/" + x + "_" + y + ".xml");
|
||||
|
||||
// Write info to XML
|
||||
final String spawnId = String.valueOf(spawn.getId());
|
||||
final String spawnCount = String.valueOf(spawn.getAmount());
|
||||
final String spawnX = String.valueOf(spawn.getX());
|
||||
final String spawnY = String.valueOf(spawn.getY());
|
||||
final String spawnZ = String.valueOf(spawn.getZ());
|
||||
final String spawnHeading = String.valueOf(spawn.getHeading());
|
||||
final String spawnDelay = String.valueOf(spawn.getRespawnDelay() / 1000);
|
||||
if (spawnFile.exists()) // update
|
||||
{
|
||||
LOGGER.log(Level.WARNING, getClass().getSimpleName() + ": Could not store spawn in the DB:" + e.getMessage(), e);
|
||||
final File tempFile = new File(spawnFile.getAbsolutePath().substring(Config.DATAPACK_ROOT.getAbsolutePath().length() + 1).replace('\\', '/') + ".tmp");
|
||||
try
|
||||
{
|
||||
final BufferedReader reader = new BufferedReader(new FileReader(spawnFile));
|
||||
final BufferedWriter writer = new BufferedWriter(new FileWriter(tempFile));
|
||||
String currentLine;
|
||||
while ((currentLine = reader.readLine()) != null)
|
||||
{
|
||||
if (currentLine.contains("</spawn>"))
|
||||
{
|
||||
writer.write(" <npc id=\"" + spawnId + (spawn.getAmount() > 1 ? "\" count=\"" + spawnCount : "") + "\" x=\"" + spawnX + "\" y=\"" + spawnY + "\" z=\"" + spawnZ + (spawn.getHeading() > 0 ? "\" heading=\"" + spawnHeading : "") + "\" respawnDelay=\"" + spawnDelay + "\" /> <!-- " + NpcData.getInstance().getTemplate(spawn.getId()).getName() + " -->" + Config.EOL);
|
||||
writer.write(currentLine + Config.EOL);
|
||||
continue;
|
||||
}
|
||||
writer.write(currentLine + Config.EOL);
|
||||
}
|
||||
writer.close();
|
||||
reader.close();
|
||||
spawnFile.delete();
|
||||
tempFile.renameTo(spawnFile);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
LOGGER.warning(getClass().getSimpleName() + ": Could not store spawn in the spawn XML files: " + e);
|
||||
}
|
||||
}
|
||||
else // new file
|
||||
{
|
||||
try
|
||||
{
|
||||
final BufferedWriter writer = new BufferedWriter(new FileWriter(spawnFile));
|
||||
writer.write("<?xml version=\"1.0\" encoding=\"UTF-8\"?>" + Config.EOL);
|
||||
writer.write("<list enabled=\"true\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:noNamespaceSchemaLocation=\"../../xsd/spawns.xsd\">" + Config.EOL);
|
||||
writer.write(" <spawn name=\"" + x + "_" + y + "\">" + Config.EOL);
|
||||
writer.write(" <npc id=\"" + spawnId + (spawn.getAmount() > 1 ? "\" count=\"" + spawnCount : "") + "\" x=\"" + spawnX + "\" y=\"" + spawnY + "\" z=\"" + spawnZ + (spawn.getHeading() > 0 ? "\" heading=\"" + spawnHeading : "") + "\" respawnDelay=\"" + spawnDelay + "\" /> <!-- " + NpcData.getInstance().getTemplate(spawn.getId()).getName() + " -->" + Config.EOL);
|
||||
writer.write(" </spawn>" + Config.EOL);
|
||||
writer.write("</list>" + Config.EOL);
|
||||
writer.close();
|
||||
LOGGER.info(getClass().getSimpleName() + ": Created file: " + OTHER_XML_FOLDER + "/" + x + "_" + y + ".xml");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
LOGGER.warning(getClass().getSimpleName() + ": Spawn " + spawn + " could not be added to the spawn XML files: " + e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -443,30 +463,84 @@ public final class SpawnTable implements IGameXmlReader
|
||||
/**
|
||||
* Delete an spawn from the spawn table.
|
||||
* @param spawn the spawn to delete
|
||||
* @param updateDb if {@code true} database will be updated
|
||||
* @param update if {@code true} the spawn XML files will be updated
|
||||
*/
|
||||
public void deleteSpawn(L2Spawn spawn, boolean updateDb)
|
||||
public void deleteSpawn(L2Spawn spawn, boolean update)
|
||||
{
|
||||
if (!removeSpawn(spawn))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (updateDb)
|
||||
if (update)
|
||||
{
|
||||
try (Connection con = DatabaseFactory.getInstance().getConnection();
|
||||
PreparedStatement delete = con.prepareStatement("DELETE FROM " + (spawn.isCustom() ? "custom_spawnlist" : "spawnlist") + " WHERE locx=? AND locy=? AND locz=? AND npc_templateid=? AND heading=?"))
|
||||
final int x = ((spawn.getX() - L2World.MAP_MIN_X) >> 15) + L2World.TILE_X_MIN;
|
||||
final int y = ((spawn.getY() - L2World.MAP_MIN_Y) >> 15) + L2World.TILE_Y_MIN;
|
||||
final int npcSpawnTemplateId = spawn.getNpcSpawnTemplateId();
|
||||
final File spawnFile = npcSpawnTemplateId != -1 ? new File(_spawnTemplates.get(npcSpawnTemplateId)) : new File(OTHER_XML_FOLDER + "/" + x + "_" + y + ".xml");
|
||||
final File tempFile = new File(spawnFile.getAbsolutePath().substring(Config.DATAPACK_ROOT.getAbsolutePath().length() + 1).replace('\\', '/') + ".tmp");
|
||||
try
|
||||
{
|
||||
delete.setInt(1, spawn.getX());
|
||||
delete.setInt(2, spawn.getY());
|
||||
delete.setInt(3, spawn.getZ());
|
||||
delete.setInt(4, spawn.getId());
|
||||
delete.setInt(5, spawn.getHeading());
|
||||
delete.execute();
|
||||
final BufferedReader reader = new BufferedReader(new FileReader(spawnFile));
|
||||
final BufferedWriter writer = new BufferedWriter(new FileWriter(tempFile));
|
||||
final String spawnId = String.valueOf(spawn.getId());
|
||||
final String spawnX = String.valueOf(spawn.getX());
|
||||
final String spawnY = String.valueOf(spawn.getY());
|
||||
final String spawnZ = String.valueOf(spawn.getZ());
|
||||
boolean found = false; // in XML you can have more than one spawn with same coords
|
||||
boolean isMultiLine = false; // in case spawn has more stats
|
||||
boolean lastLineFound = false; // used to check for empty file
|
||||
int lineCount = 0;
|
||||
String currentLine;
|
||||
while ((currentLine = reader.readLine()) != null)
|
||||
{
|
||||
if (!found)
|
||||
{
|
||||
if (isMultiLine)
|
||||
{
|
||||
if (currentLine.contains("</npc>"))
|
||||
{
|
||||
found = true;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (currentLine.contains(spawnId) && currentLine.contains(spawnX) && currentLine.contains(spawnY) && currentLine.contains(spawnZ))
|
||||
{
|
||||
if (!currentLine.contains("/>") && !currentLine.contains("</npc>"))
|
||||
{
|
||||
isMultiLine = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
found = true;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
writer.write(currentLine + Config.EOL);
|
||||
if (currentLine.contains("</list>"))
|
||||
{
|
||||
lastLineFound = true;
|
||||
}
|
||||
if (!lastLineFound)
|
||||
{
|
||||
lineCount++;
|
||||
}
|
||||
}
|
||||
writer.close();
|
||||
reader.close();
|
||||
spawnFile.delete();
|
||||
tempFile.renameTo(spawnFile);
|
||||
// Delete empty file
|
||||
if (lineCount < 7)
|
||||
{
|
||||
LOGGER.info(getClass().getSimpleName() + ": Deleted empty file: " + spawnFile.getAbsolutePath().substring(Config.DATAPACK_ROOT.getAbsolutePath().length() + 1).replace('\\', '/'));
|
||||
spawnFile.delete();
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
LOGGER.log(Level.WARNING, getClass().getSimpleName() + ": Spawn " + spawn + " could not be removed from DB: " + e.getMessage(), e);
|
||||
LOGGER.warning(getClass().getSimpleName() + ": Spawn " + spawn + " could not be removed from the spawn XML files: " + e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -521,6 +595,11 @@ public final class SpawnTable implements IGameXmlReader
|
||||
return true;
|
||||
}
|
||||
|
||||
public String getSpawnFile(int templateId)
|
||||
{
|
||||
return _spawnTemplates.get(templateId);
|
||||
}
|
||||
|
||||
public static SpawnTable getInstance()
|
||||
{
|
||||
return SingletonHolder._instance;
|
||||
|
@ -78,12 +78,11 @@ public class L2Spawn implements IPositionable, IIdentifiable, INamable
|
||||
private Constructor<? extends L2Npc> _constructor;
|
||||
/** If True a L2NpcInstance is respawned each time that another is killed */
|
||||
private boolean _doRespawn;
|
||||
/** If true then spawn is custom */
|
||||
private boolean _customSpawn;
|
||||
private static List<SpawnListener> _spawnListeners = new CopyOnWriteArrayList<>();
|
||||
private final Deque<L2Npc> _spawnedNpcs = new ConcurrentLinkedDeque<>();
|
||||
private Map<Integer, Location> _lastSpawnPoints;
|
||||
private boolean _isNoRndWalk = false; // Is no random walk
|
||||
private int _spawnTemplateId = 0;
|
||||
|
||||
/** The task launching the function doSpawn() */
|
||||
class SpawnTask implements Runnable
|
||||
@ -389,23 +388,6 @@ public class L2Spawn implements IPositionable, IIdentifiable, INamable
|
||||
_respawnMaxDelay = date;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the spawn as custom.<BR>
|
||||
* @param custom
|
||||
*/
|
||||
public void setCustom(boolean custom)
|
||||
{
|
||||
_customSpawn = custom;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return type of spawn.
|
||||
*/
|
||||
public boolean isCustom()
|
||||
{
|
||||
return _customSpawn;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrease the current number of L2NpcInstance of this L2Spawn and if necessary create a SpawnTask to launch after the respawn Delay. <B><U> Actions</U> :</B>
|
||||
* <li>Decrease the current number of L2NpcInstance of this L2Spawn</li>
|
||||
@ -556,10 +538,10 @@ public class L2Spawn implements IPositionable, IIdentifiable, INamable
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mob
|
||||
* @param npc
|
||||
* @return
|
||||
*/
|
||||
private L2Npc initializeNpcInstance(L2Npc mob)
|
||||
private L2Npc initializeNpcInstance(L2Npc npc)
|
||||
{
|
||||
int newlocx = 0;
|
||||
int newlocy = 0;
|
||||
@ -579,7 +561,7 @@ public class L2Spawn implements IPositionable, IIdentifiable, INamable
|
||||
{
|
||||
if (getLocationId() == 0)
|
||||
{
|
||||
return mob;
|
||||
return npc;
|
||||
}
|
||||
|
||||
// Calculate the random position in the location area
|
||||
@ -607,8 +589,8 @@ public class L2Spawn implements IPositionable, IIdentifiable, INamable
|
||||
final int randX = newlocx + Rnd.get(Config.MOB_MIN_SPAWN_RANGE, Config.MOB_MAX_SPAWN_RANGE);
|
||||
final int randY = newlocy + Rnd.get(Config.MOB_MIN_SPAWN_RANGE, Config.MOB_MAX_SPAWN_RANGE);
|
||||
|
||||
final boolean isQuestMonster = (mob.getTitle() != null) && mob.getTitle().contains("Quest");
|
||||
if (mob.isMonster() && !isQuestMonster && !mob.isWalker() && !mob.isInsideZone(ZoneId.NO_BOOKMARK) && (getInstanceId() == 0) && GeoEngine.getInstance().canMoveToTarget(newlocx, newlocy, newlocz, randX, randY, newlocz, getInstanceId()) && !getTemplate().isUndying() && !mob.isRaid() && !mob.isRaidMinion() && !Config.MOBS_LIST_NOT_RANDOM.contains(mob.getId()))
|
||||
final boolean isQuestMonster = (npc.getTitle() != null) && npc.getTitle().contains("Quest");
|
||||
if (npc.isMonster() && !isQuestMonster && !npc.isWalker() && !npc.isInsideZone(ZoneId.NO_BOOKMARK) && (getInstanceId() == 0) && GeoEngine.getInstance().canMoveToTarget(newlocx, newlocy, newlocz, randX, randY, newlocz, getInstanceId()) && !getTemplate().isUndying() && !npc.isRaid() && !npc.isRaidMinion() && !Config.MOBS_LIST_NOT_RANDOM.contains(npc.getId()))
|
||||
{
|
||||
newlocx = randX;
|
||||
newlocy = randY;
|
||||
@ -622,69 +604,69 @@ public class L2Spawn implements IPositionable, IIdentifiable, INamable
|
||||
// newlocz = GeoEngine.getInstance().getHeight(newlocx, newlocy, newlocz);
|
||||
// }
|
||||
|
||||
mob.stopAllEffects();
|
||||
npc.stopAllEffects();
|
||||
|
||||
mob.setIsDead(false);
|
||||
npc.setIsDead(false);
|
||||
// Reset decay info
|
||||
mob.setDecayed(false);
|
||||
npc.setDecayed(false);
|
||||
// Set the HP and MP of the L2NpcInstance to the max
|
||||
mob.setCurrentHpMp(mob.getMaxHp(), mob.getMaxMp());
|
||||
npc.setCurrentHpMp(npc.getMaxHp(), npc.getMaxMp());
|
||||
// Clear script variables
|
||||
if (mob.hasVariables())
|
||||
if (npc.hasVariables())
|
||||
{
|
||||
mob.getVariables().getSet().clear();
|
||||
npc.getVariables().getSet().clear();
|
||||
}
|
||||
// Set is not random walk default value
|
||||
mob.setIsNoRndWalk(isNoRndWalk());
|
||||
npc.setIsNoRndWalk(isNoRndWalk());
|
||||
|
||||
// Set the heading of the L2NpcInstance (random heading if not defined)
|
||||
if (getHeading() == -1)
|
||||
{
|
||||
mob.setHeading(Rnd.nextInt(61794));
|
||||
npc.setHeading(Rnd.nextInt(61794));
|
||||
}
|
||||
else
|
||||
{
|
||||
mob.setHeading(getHeading());
|
||||
npc.setHeading(getHeading());
|
||||
}
|
||||
|
||||
if (mob instanceof L2Attackable)
|
||||
if (npc instanceof L2Attackable)
|
||||
{
|
||||
((L2Attackable) mob).setChampion(false);
|
||||
((L2Attackable) npc).setChampion(false);
|
||||
}
|
||||
|
||||
if (Config.L2JMOD_CHAMPION_ENABLE)
|
||||
{
|
||||
// Set champion on next spawn
|
||||
if (mob.isMonster() && !getTemplate().isUndying() && !mob.isRaid() && !mob.isRaidMinion() && (Config.L2JMOD_CHAMPION_FREQUENCY > 0) && (mob.getLevel() >= Config.L2JMOD_CHAMP_MIN_LVL) && (mob.getLevel() <= Config.L2JMOD_CHAMP_MAX_LVL) && (Config.L2JMOD_CHAMPION_ENABLE_IN_INSTANCES || (getInstanceId() == 0)))
|
||||
if (npc.isMonster() && !getTemplate().isUndying() && !npc.isRaid() && !npc.isRaidMinion() && (Config.L2JMOD_CHAMPION_FREQUENCY > 0) && (npc.getLevel() >= Config.L2JMOD_CHAMP_MIN_LVL) && (npc.getLevel() <= Config.L2JMOD_CHAMP_MAX_LVL) && (Config.L2JMOD_CHAMPION_ENABLE_IN_INSTANCES || (getInstanceId() == 0)))
|
||||
{
|
||||
if (Rnd.get(100) < Config.L2JMOD_CHAMPION_FREQUENCY)
|
||||
{
|
||||
((L2Attackable) mob).setChampion(true);
|
||||
((L2Attackable) npc).setChampion(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Reset summoner
|
||||
mob.setSummoner(null);
|
||||
npc.setSummoner(null);
|
||||
// Reset summoned list
|
||||
mob.resetSummonedNpcs();
|
||||
npc.resetSummonedNpcs();
|
||||
// Link the L2NpcInstance to this L2Spawn
|
||||
mob.setSpawn(this);
|
||||
npc.setSpawn(this);
|
||||
|
||||
// Spawn NPC
|
||||
mob.spawnMe(newlocx, newlocy, newlocz);
|
||||
npc.spawnMe(newlocx, newlocy, newlocz);
|
||||
|
||||
notifyNpcSpawned(mob);
|
||||
notifyNpcSpawned(npc);
|
||||
|
||||
_spawnedNpcs.add(mob);
|
||||
_spawnedNpcs.add(npc);
|
||||
if (_lastSpawnPoints != null)
|
||||
{
|
||||
_lastSpawnPoints.put(mob.getObjectId(), new Location(newlocx, newlocy, newlocz));
|
||||
_lastSpawnPoints.put(npc.getObjectId(), new Location(newlocx, newlocy, newlocz));
|
||||
}
|
||||
|
||||
// Increase the current number of L2NpcInstance managed by this L2Spawn
|
||||
_currentCount++;
|
||||
return mob;
|
||||
return npc;
|
||||
}
|
||||
|
||||
public static void addSpawnListener(SpawnListener listener)
|
||||
@ -801,6 +783,16 @@ public class L2Spawn implements IPositionable, IIdentifiable, INamable
|
||||
_isNoRndWalk = value;
|
||||
}
|
||||
|
||||
public void setSpawnTemplateId(int npcSpawnTemplateId)
|
||||
{
|
||||
_spawnTemplateId = npcSpawnTemplateId;
|
||||
}
|
||||
|
||||
public int getNpcSpawnTemplateId()
|
||||
{
|
||||
return _spawnTemplateId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString()
|
||||
{
|
||||
|
@ -30,3 +30,4 @@ What is done
|
||||
-New ThreadPool
|
||||
-New XML reader
|
||||
-Replaced MMOCore with Netty
|
||||
-Dropped SQL spawnlist
|
||||
|
Loading…
Reference in New Issue
Block a user