Save Location: permite guardar y organizar múltiples ubicaciones del personaje directamente desde el juego. Cada localización se almacena en un archivo TXT con sus coordenadas X, Y, Z y orientación (Heading), identificadas mediante un ID correlativo.
Incluye dos funciones:
- Guardar Location: almacena la ubicación actual del personaje en el archivo activo.
- Siguiente Línea: crea un nuevo archivo TXT y reinicia el contador de IDs.
Ideal para recopilar coordenadas de NPCs, zonas, eventos, spawns y otros puntos de interés del mundo de Lineage II.
Formato del txt generado:
id="1", x="150268", y="46253", z="-3438", h="25451";
id="2", x="150300", y="46260", z="-3430", h="25451";
id="3", x="150350", y="46300", z="-3420", h="25451";/*
* Copyright © 2026 L2JDevs
*
* 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 <https://www.gnu.org/licenses/>.
*/
package handlers.voicedcommandhandlers.custom;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.DirectoryStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.l2jdevs.gameserver.handler.IVoicedCommandHandler;
import org.l2jdevs.gameserver.model.actor.instance.L2PcInstance;
import org.l2jdevs.gameserver.network.serverpackets.NpcHtmlMessage;
/**
* Voice Command - Save Location (.savelocation)
* @author swarlog
*/
public class SaveLocation implements IVoicedCommandHandler
{
private static final Logger LOGGER = Logger.getLogger(SaveLocation.class.getName());
// Directory where locations are stored.
private static final Path SAVE_DIRECTORY = Paths.get("config", "save");
// HTML file used by this command.
private static final String HTML_FILE = "data/html/mods/handler/SaveLocation.htm";
private static final String[] VOICED_COMMANDS =
{
"savelocation",
"saveloc"
};
private static final Pattern LOCATION_ID_PATTERN = Pattern.compile("id=\"(\\d+)\"");
@Override
public boolean useVoicedCommand(String command, L2PcInstance activeChar, String target)
{
if (activeChar == null)
{
return false;
}
if ((target == null) || target.trim().isEmpty())
{
showHtml(activeChar);
return true;
}
String action = target.trim().toLowerCase();
switch (action)
{
case "save":
saveLocation(activeChar);
return true;
case "next":
nextFile(activeChar);
return true;
default:
showHtml(activeChar);
return true;
}
}
private void showHtml(L2PcInstance activeChar)
{
final NpcHtmlMessage html = new NpcHtmlMessage(0);
html.setFile(activeChar.getHtmlPrefix(), HTML_FILE);
activeChar.sendPacket(html);
}
/**
* Saves the player's current location to the active TXT file. Each location is assigned a sequential ID.
*/
private synchronized void saveLocation(L2PcInstance activeChar)
{
try
{
Files.createDirectories(SAVE_DIRECTORY);
String playerName = getSafePlayerName(activeChar);
int fileIndex = getCurrentFileIndex(playerName);
Path file = getLocationFile(playerName, fileIndex);
// Create the first file if it does not exist yet.
if (!Files.exists(file))
{
Files.createFile(file);
}
int nextId = getNextLocationId(file);
String location = "id=\"" + nextId + "\", " + "x=\"" + activeChar.getX() + "\", " + "y=\"" + activeChar.getY() + "\", " + "z=\"" + activeChar.getZ() + "\", " + "h=\"" + activeChar.getHeading() + "\";";
Files.write(file, (location + System.lineSeparator()).getBytes(StandardCharsets.UTF_8), StandardOpenOption.CREATE, StandardOpenOption.APPEND);
activeChar.sendMessage("Localizacion guardada correctamente. ID: " + nextId);
}
catch (Exception e)
{
LOGGER.log(Level.SEVERE, "Error guardando la localizacion de " + activeChar.getName(), e);
activeChar.sendMessage("Error al guardar la localizacion: " + e.getMessage());
}
}
/**
* Creates a new TXT file. The location ID counter always starts again from 1.
*/
private synchronized void nextFile(L2PcInstance activeChar)
{
try
{
Files.createDirectories(SAVE_DIRECTORY);
String playerName = getSafePlayerName(activeChar);
int currentIndex = getCurrentFileIndex(playerName);
int newIndex = currentIndex + 1;
Path newFile = getLocationFile(playerName, newIndex);
Files.createFile(newFile);
activeChar.sendMessage("Nuevo archivo de localizaciones creado: " + newFile.getFileName());
activeChar.sendMessage("El contador de localizaciones comienza de nuevo en ID 1.");
}
catch (Exception e)
{
LOGGER.log(Level.SEVERE, "Error creando un nuevo archivo de localizaciones para " + activeChar.getName(), e);
activeChar.sendMessage("Error al crear el nuevo archivo: " + e.getMessage());
}
}
/**
* Gets the index of the player's latest TXT file. Returns 0 if no file exists.
*/
private int getCurrentFileIndex(String playerName) throws IOException
{
int maxIndex = 0;
String prefix = "location_" + playerName + "_";
Pattern filePattern = Pattern.compile("^" + Pattern.quote(prefix) + "(\\d+)\\.txt$");
try (DirectoryStream<Path> files = Files.newDirectoryStream(SAVE_DIRECTORY))
{
for (Path file : files)
{
Matcher matcher = filePattern.matcher(file.getFileName().toString());
if (matcher.matches() && Files.isRegularFile(file))
{
int index = Integer.parseInt(matcher.group(1));
if (index > maxIndex)
{
maxIndex = index;
}
}
}
}
return maxIndex;
}
/**
* Calculates the next location ID by reading the contents of the current file.
*/
private int getNextLocationId(Path file) throws IOException
{
if (!Files.exists(file))
{
return 1;
}
List<String> lines = Files.readAllLines(file, StandardCharsets.UTF_8);
int maxId = 0;
for (String line : lines)
{
Matcher matcher = LOCATION_ID_PATTERN.matcher(line);
if (matcher.find())
{
int id = Integer.parseInt(matcher.group(1));
if (id > maxId)
{
maxId = id;
}
}
}
return maxId + 1;
}
/**
* Builds the file path for the specified player.
*/
private Path getLocationFile(String playerName, int fileIndex)
{
return SAVE_DIRECTORY.resolve("location_" + playerName + "_" + fileIndex + ".txt");
}
/**
* Sanitizes the player's name so it can safely be used in a file name.
*/
private String getSafePlayerName(L2PcInstance activeChar)
{
return activeChar.getName().replaceAll("[^a-zA-Z0-9_-]", "_");
}
@Override
public String[] getVoicedCommandList()
{
return VOICED_COMMANDS;
}
}
<html>
<body>
<center>
<br>
<font name="hs12" color="LEVEL">LOCATION SAVER</font>
<br>
<img src="L2UI.SquareWhite" width=280 height=1>
<br>
<table width=280>
<tr>
<td align=center>
<font color="FFFFFF">Linea actual:</font>
<font color="LEVEL">%line%</font>
</td>
</tr>
</table>
<br>
<table width=220>
<tr>
<td align=center>
<button value="GUARDAR LOCATION" action="bypass -h voice .savelocation save" width=180 height=35 back="L2UI_ct1.button_df" fore="L2UI_ct1.button_df">
</td>
</tr>
<tr>
<td align=center>
<button value="SIGUIENTE LINEA" action="bypass -h voice .savelocation next" width=180 height=35 back="L2UI_ct1.button_df" fore="L2UI_ct1.button_df">
</td>
</tr>
</table>
<br>
<img src="L2UI.SquareWhite" width=280 height=1>
<br>
<font color="AAAAAA">Location Saver - L2JDevs</font>
</center>
</body>
</html>