L2JMobius
Public Development => Shares/Contributions => Archived User Contributions => Topic started by: javierdc on July 03, 2025, 06:08:20 PM
-
Players can now leave their characters auto-farming using the .offlineplay command, even without having the game client open.
🧠 But the most powerful part of this system is that it stores all essential character data in the database, so that after a server restart, the character:
✅ Is automatically loaded from the database
✅ Respawns at the exact same coordinates (x, y, z) and heading
✅ Restores current HP, MP, and CP
✅ Recovers auto-used skill list (Auto Skills)
✅ Recovers full AutoPlay, AutoUse, and AutoSupply configurations
✅ Re-enables Soulshots and Blessed Spiritshots automatically
✅ Continues in offline play mode without any user action
🗄️ What is saved to the database (table character_offline_play) when .offlineplay is activated?
Character ID (charId)
Coordinates: x, y, z, and heading
Current HP, MP, and CP
List of auto-used skills
AutoPlay settings
AutoUse settings (skills and items)
AutoSupply items
Offline mode start timestamp
🛡️ This feature ensures your character keeps farming even after server reboots, making it perfect for continuous, worry-free progression.
package org.l2jmobius.gameserver.data.sql;
import java.sql.*;
import java.util.Arrays;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.l2jmobius.commons.database.DatabaseFactory;
import org.l2jmobius.gameserver.model.World;
import org.l2jmobius.gameserver.model.actor.Player;
import org.l2jmobius.gameserver.taskmanager.AutoPlayTaskManager;
import org.l2jmobius.gameserver.taskmanager.AutoUseTaskManager;
public class OfflinePlayTable
{
private static final Logger LOGGER = Logger.getLogger(OfflinePlayTable.class.getName());
// SQL DEFINITIONS
private static final String CLEAR_OFFLINE_PLAY = "DELETE FROM character_offline_play";
private static final String INSERT_STATE =
"INSERT INTO character_offline_play " +
"(charId,x,y,z,heading,offline_hp,offline_mp,offline_cp," +
"offline_autoskills,offline_autoplay,offline_autouse,offline_autosupply,offline_start) " +
"VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?)";
private static final String LOAD_STATE = "SELECT * FROM character_offline_play";
protected OfflinePlayTable()
{
}
public void storeOfflinePlayers()
{
try (Connection con = DatabaseFactory.getConnection();
PreparedStatement clear = con.prepareStatement(CLEAR_OFFLINE_PLAY);
PreparedStatement insert = con.prepareStatement(INSERT_STATE))
{
// 1) Limpio tabla
clear.execute();
// 2) Inserto cada jugador en offlineplay
for (Player pc : World.getInstance().getPlayers())
{
if (!pc.isOfflinePlay())
{
continue;
}
insert.setInt(1, pc.getObjectId());
insert.setInt(2, pc.getX());
insert.setInt(3, pc.getY());
insert.setInt(4, pc.getZ());
insert.setInt(5, pc.getHeading());
insert.setDouble(6, pc.getCurrentHp());
insert.setDouble(7, pc.getCurrentMp());
insert.setDouble(8, pc.getCurrentCp());
insert.setString(9, serializeAutoSkills(pc));
insert.setString(10, pc.getAutoPlaySettings().serialize());
insert.setString(11, pc.getAutoUseSettings().serialize());
insert.setString(12, serializeAutoSupplyItems(pc));
insert.setLong(13, pc.getOfflineStartTime());
insert.executeUpdate();
}
}
catch (Exception e)
{
LOGGER.log(Level.WARNING, "Error while saving offline play players.", e);
}
}
public void restoreOfflinePlayers()
{
LOGGER.info("OfflinePlayTable: Restoring offlineplay players...");
try (Connection con = DatabaseFactory.getConnection();
Statement stm = con.createStatement();
ResultSet rs = stm.executeQuery(LOAD_STATE))
{
while (rs.next())
{
final int charId = rs.getInt("charId");
final Player pc = Player.load(charId);
if (pc == null)
{
continue;
}
// Posición y stats
pc.setXYZ(rs.getInt("x"), rs.getInt("y"), rs.getInt("z"));
pc.setHeading(rs.getInt("heading"));
pc.setCurrentHp(rs.getDouble("offline_hp"));
pc.setCurrentMp(rs.getDouble("offline_mp"));
pc.setCurrentCp(rs.getDouble("offline_cp"));
// Restaurar configuración
restoreAutoSkills(pc, rs.getString("offline_autoskills"));
pc.getAutoPlaySettings().deserialize(rs.getString("offline_autoplay"));
pc.getAutoUseSettings().deserialize(rs.getString("offline_autouse"), pc);
// Fuerza auto-use de soulshots tras cargar config
// Activar soulshots por defecto al restaurar en modo offlineplay
pc.addAutoSoulShot(91927); // Soulshot
pc.addAutoSoulShot(91930); // Blessed Spiritshot
pc.rechargeShots(true, true, true); // Cargar los shots si tiene en inventario
pc.restoreAutoSettings();
//pc.forceAutoUseShots();
// Reactivar offline play
pc.startOfflinePlay();
pc.spawnMe();
AutoPlayTaskManager.getInstance().startAutoPlay(pc);
LOGGER.info("Restored offline play player: " + pc.getName());
}
}
catch (Exception e)
{
LOGGER.log(Level.WARNING, "Error while restoring offline play players.", e);
}
}
private String serializeAutoSkills(Player pc)
{
return String.join(",",
pc.getAutoUseSettings().getAutoSkills().stream()
.map(Object::toString)
.toArray(String[]::new));
}
private void restoreAutoSkills(Player pc, String csv)
{
if (csv == null || csv.isEmpty())
{
return;
}
Arrays.stream(csv.split(","))
.map(Integer::parseInt)
.forEach(id -> pc.getAutoUseSettings().getAutoSkills().add(id));
}
private String serializeAutoSupplyItems(Player pc)
{
return String.join(",",
pc.getAutoUseSettings().getAutoSupplyItems().stream()
.map(Object::toString)
.toArray(String[]::new));
}
private void restoreAutoSupplyItems(Player pc, String csv)
{
if (csv == null || csv.isEmpty())
{
return;
}
Arrays.stream(csv.split(","))
.map(Integer::parseInt)
.forEach(id -> pc.getAutoUseSettings().getAutoSupplyItems().add(id));
}
public static OfflinePlayTable getInstance()
{
return SingletonHolder.INSTANCE;
}
private static class SingletonHolder
{
protected static final OfflinePlayTable INSTANCE = new OfflinePlayTable();
}
}
SET FOREIGN_KEY_CHECKS=0;
-- ----------------------------
-- Table structure for character_offline_play
-- ----------------------------
CREATE TABLE `character_offline_play` (
`charId` int(11) NOT NULL,
`x` int(11) NOT NULL,
`y` int(11) NOT NULL,
`z` int(11) NOT NULL,
`heading` int(11) NOT NULL,
`offline_hp` double NOT NULL,
`offline_mp` double NOT NULL,
`offline_cp` double NOT NULL,
`offline_autoskills` text DEFAULT NULL,
`offline_autoplay` text DEFAULT NULL,
`offline_autouse` text DEFAULT NULL,
`offline_start` bigint(20) NOT NULL,
`offline_autosupply` text DEFAULT NULL,
PRIMARY KEY (`charId`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_general_ci;
-- ----------------------------
-- Records
-- ----------------------------
in AutoUseSettingsHolder.java
// --- Métodos para persistencia ---
public String serialize()
{
String autoSupply = _autoSupplyItems.stream()
.map(id -> {
ItemTemplate item = ItemData.getInstance().getTemplate(id);
if (item != null)
{
ActionType action = item.getDefaultAction();
int type = switch (action)
{
case SOULSHOT -> 16;
case SPIRITSHOT -> 17;
case SUMMON_SOULSHOT -> 18;
case SUMMON_SPIRITSHOT -> 19;
default -> 0;
};
return id + ":" + type; // ← SEPARADOR CAMBIADO
}
else
{
return id + ":0";
}
})
.collect(Collectors.joining(","));
return String.format(
"%s|%s|%s|%s|%s|%s",
autoSupply,
_autoActions.stream().map(Object::toString).collect(Collectors.joining(",")),
_autoBuffs.stream().map(Object::toString).collect(Collectors.joining(",")),
_autoSkills.stream().map(Object::toString).collect(Collectors.joining(",")),
_autoPotionItem.get(),
_autoPetPotionItem.get()
);
}
public void deserialize(String data, Player player)
{
if (data == null || data.isEmpty())
{
return;
}
try
{
String[] parts = data.split("\\|", -1);
_autoSupplyItems.clear();
if (parts.length > 0 && !parts[0].isEmpty())
{
String[] entries = parts[0].split(",");
for (String entry : entries)
{
String[] split = entry.split(":");
int itemId = Integer.parseInt(split[0]);
int type = (split.length > 1) ? Integer.parseInt(split[1]) : 0;
_autoSupplyItems.add(itemId);
// Restaurar comportamiento según tipo
switch (type)
{
case 16, 17, 18, 19 -> AutoUseTaskManager.getInstance().addAutoSupplyItem(player, itemId);
}
}
}
_autoActions.clear();
if (parts.length > 1 && !parts[1].isEmpty())
{
Arrays.stream(parts[1].split(",")).map(Integer::parseInt).forEach(_autoActions::add);
}
_autoBuffs.clear();
if (parts.length > 2 && !parts[2].isEmpty())
{
Arrays.stream(parts[2].split(",")).map(Integer::parseInt).forEach(_autoBuffs::add);
}
_autoSkills.clear();
if (parts.length > 3 && !parts[3].isEmpty())
{
Arrays.stream(parts[3].split(",")).map(Integer::parseInt).forEach(_autoSkills::add);
}
if (parts.length > 4 && !parts[4].isEmpty())
{
_autoPotionItem.set(Integer.parseInt(parts[4]));
}
if (parts.length > 5 && !parts[5].isEmpty())
{
_autoPetPotionItem.set(Integer.parseInt(parts[5]));
}
}
catch (Exception e)
{
LOGGER.log(Level.WARNING, "Failed to deserialize AutoUseSettings: " + data, e);
}
}
-
All comments are not in English.
Code format does not comply with project standards.
OfflinePlayTable restoreAutoSupplyItems method is not used.
You did not provide AutoPlaySettingsHolder serialize and deserialize methods.
Also wont work with all projects.
-
Smell like IA...
-
pd: a mi me funciona
-
Already mentioned that you miss methods needed for this to make it actually work.
This is a bad AI implementation at best.
https://l2jmobius.org/forum/index.php?topic=13898.0
You purposely do not use English.
https://l2jmobius.org/forum/index.php?topic=22
Topic archived.