From ce038fd2d0bbe428c41d2ca370df5bdeb623ae7f Mon Sep 17 00:00:00 2001
From: HorridoJoho <HorridoJoho@l2jserver.com>
Date: Sun, 30 Aug 2020 22:00:54 +0200
Subject: [PATCH] Buffer Service for L2J

Requires CustomNpcData=True in general.properties.
Requires l2j-server-game update for config/bufferservice.properties.

Default npc in data/stats/npcs/custom/custom_bufferservice.xml (id
60001).

Take 'Divine Inspiration' into consideration when calculating max buffs
per unique bufflist.
Suggested by @JMD
---
 .../service/base/CustomServiceScript.java     | 214 +++++++
 .../custom/service/base/DialogType.java       |  29 +
 .../model/entity/CustomServiceProduct.java    |  51 ++
 .../model/entity/CustomServiceServer.java     |  71 +++
 .../service/base/model/entity/IRefable.java   |  29 +
 .../base/model/entity/ItemRequirement.java    |  72 +++
 .../service/base/model/entity/Refable.java    |  60 ++
 .../service/base/util/CommandProcessor.java   |  55 ++
 .../base/util/htmltmpls/HTMLTemplateFunc.java |  88 +++
 .../util/htmltmpls/HTMLTemplateParser.java    | 154 +++++
 .../htmltmpls/HTMLTemplatePlaceholder.java    | 123 ++++
 .../util/htmltmpls/HTMLTemplateUtils.java     | 129 +++++
 .../htmltmpls/funcs/ChildrenCountFunc.java    |  49 ++
 .../base/util/htmltmpls/funcs/ExistsFunc.java |  85 +++
 .../util/htmltmpls/funcs/ForeachFunc.java     | 102 ++++
 .../util/htmltmpls/funcs/IfChildrenFunc.java  | 144 +++++
 .../base/util/htmltmpls/funcs/IfFunc.java     | 136 +++++
 .../util/htmltmpls/funcs/IncludeFunc.java     |  50 ++
 .../custom/service/buffer/BufferService.java  | 539 ++++++++++++++++++
 .../buffer/BufferServiceBypassHandler.java    |  65 +++
 .../buffer/BufferServiceItemHandler.java      |  54 ++
 .../buffer/BufferServiceRepository.java       | 260 +++++++++
 .../BufferServiceVoicedCommandHandler.java    |  58 ++
 .../service/buffer/model/BufferConfig.java    | 116 ++++
 .../service/buffer/model/GlobalConfig.java    |  56 ++
 .../service/buffer/model/UniqueBufflist.java  |  87 +++
 .../buffer/model/entity/AbstractBuffer.java   |  97 ++++
 .../buffer/model/entity/BuffCategory.java     |  68 +++
 .../buffer/model/entity/BuffSkill.java        |  83 +++
 .../buffer/model/entity/NpcBuffer.java        |  64 +++
 .../buffer/model/entity/VoicedBuffer.java     |  49 ++
 .../Q00325_GrimCollector.java                 |  10 +-
 src/main/resources/data/scripts.cfg           |   1 +
 .../buffer/html/community/category.html       |  27 +
 .../community/inc/active_unique_table.html    |  10 +
 .../buffer/html/community/inc/header.html     |  36 ++
 .../service/buffer/html/community/main.html   |  20 +
 .../service/buffer/html/npc/category.html     |  42 ++
 .../data/service/buffer/html/npc/main.html    |  67 +++
 .../data/service/buffer/html/npc/unique.html  |  17 +
 .../service/buffer/json/documentation.txt     |  85 +++
 .../data/service/buffer/json/global.json      | 197 +++++++
 .../data/service/buffer/json/npcs/60001.json  |  11 +
 .../data/service/buffer/json/voiced.json      |   9 +
 .../npcs/custom/custom_bufferservice.xml      |   9 +
 .../custom/custom_buffer_service_1_ulists.sql |   8 +
 .../custom_buffer_service_2_ulist_buffs.sql   |   6 +
 47 files changed, 3787 insertions(+), 5 deletions(-)
 create mode 100644 src/main/java/com/l2jserver/datapack/custom/service/base/CustomServiceScript.java
 create mode 100644 src/main/java/com/l2jserver/datapack/custom/service/base/DialogType.java
 create mode 100644 src/main/java/com/l2jserver/datapack/custom/service/base/model/entity/CustomServiceProduct.java
 create mode 100644 src/main/java/com/l2jserver/datapack/custom/service/base/model/entity/CustomServiceServer.java
 create mode 100644 src/main/java/com/l2jserver/datapack/custom/service/base/model/entity/IRefable.java
 create mode 100644 src/main/java/com/l2jserver/datapack/custom/service/base/model/entity/ItemRequirement.java
 create mode 100644 src/main/java/com/l2jserver/datapack/custom/service/base/model/entity/Refable.java
 create mode 100644 src/main/java/com/l2jserver/datapack/custom/service/base/util/CommandProcessor.java
 create mode 100644 src/main/java/com/l2jserver/datapack/custom/service/base/util/htmltmpls/HTMLTemplateFunc.java
 create mode 100644 src/main/java/com/l2jserver/datapack/custom/service/base/util/htmltmpls/HTMLTemplateParser.java
 create mode 100644 src/main/java/com/l2jserver/datapack/custom/service/base/util/htmltmpls/HTMLTemplatePlaceholder.java
 create mode 100644 src/main/java/com/l2jserver/datapack/custom/service/base/util/htmltmpls/HTMLTemplateUtils.java
 create mode 100644 src/main/java/com/l2jserver/datapack/custom/service/base/util/htmltmpls/funcs/ChildrenCountFunc.java
 create mode 100644 src/main/java/com/l2jserver/datapack/custom/service/base/util/htmltmpls/funcs/ExistsFunc.java
 create mode 100644 src/main/java/com/l2jserver/datapack/custom/service/base/util/htmltmpls/funcs/ForeachFunc.java
 create mode 100644 src/main/java/com/l2jserver/datapack/custom/service/base/util/htmltmpls/funcs/IfChildrenFunc.java
 create mode 100644 src/main/java/com/l2jserver/datapack/custom/service/base/util/htmltmpls/funcs/IfFunc.java
 create mode 100644 src/main/java/com/l2jserver/datapack/custom/service/base/util/htmltmpls/funcs/IncludeFunc.java
 create mode 100644 src/main/java/com/l2jserver/datapack/custom/service/buffer/BufferService.java
 create mode 100644 src/main/java/com/l2jserver/datapack/custom/service/buffer/BufferServiceBypassHandler.java
 create mode 100644 src/main/java/com/l2jserver/datapack/custom/service/buffer/BufferServiceItemHandler.java
 create mode 100644 src/main/java/com/l2jserver/datapack/custom/service/buffer/BufferServiceRepository.java
 create mode 100644 src/main/java/com/l2jserver/datapack/custom/service/buffer/BufferServiceVoicedCommandHandler.java
 create mode 100644 src/main/java/com/l2jserver/datapack/custom/service/buffer/model/BufferConfig.java
 create mode 100644 src/main/java/com/l2jserver/datapack/custom/service/buffer/model/GlobalConfig.java
 create mode 100644 src/main/java/com/l2jserver/datapack/custom/service/buffer/model/UniqueBufflist.java
 create mode 100644 src/main/java/com/l2jserver/datapack/custom/service/buffer/model/entity/AbstractBuffer.java
 create mode 100644 src/main/java/com/l2jserver/datapack/custom/service/buffer/model/entity/BuffCategory.java
 create mode 100644 src/main/java/com/l2jserver/datapack/custom/service/buffer/model/entity/BuffSkill.java
 create mode 100644 src/main/java/com/l2jserver/datapack/custom/service/buffer/model/entity/NpcBuffer.java
 create mode 100644 src/main/java/com/l2jserver/datapack/custom/service/buffer/model/entity/VoicedBuffer.java
 create mode 100644 src/main/resources/data/service/buffer/html/community/category.html
 create mode 100644 src/main/resources/data/service/buffer/html/community/inc/active_unique_table.html
 create mode 100644 src/main/resources/data/service/buffer/html/community/inc/header.html
 create mode 100644 src/main/resources/data/service/buffer/html/community/main.html
 create mode 100644 src/main/resources/data/service/buffer/html/npc/category.html
 create mode 100644 src/main/resources/data/service/buffer/html/npc/main.html
 create mode 100644 src/main/resources/data/service/buffer/html/npc/unique.html
 create mode 100644 src/main/resources/data/service/buffer/json/documentation.txt
 create mode 100644 src/main/resources/data/service/buffer/json/global.json
 create mode 100644 src/main/resources/data/service/buffer/json/npcs/60001.json
 create mode 100644 src/main/resources/data/service/buffer/json/voiced.json
 create mode 100644 src/main/resources/data/stats/npcs/custom/custom_bufferservice.xml
 create mode 100644 src/main/resources/sql/custom/custom_buffer_service_1_ulists.sql
 create mode 100644 src/main/resources/sql/custom/custom_buffer_service_2_ulist_buffs.sql

diff --git a/src/main/java/com/l2jserver/datapack/custom/service/base/CustomServiceScript.java b/src/main/java/com/l2jserver/datapack/custom/service/base/CustomServiceScript.java
new file mode 100644
index 0000000000..2c9d6d27ad
--- /dev/null
+++ b/src/main/java/com/l2jserver/datapack/custom/service/base/CustomServiceScript.java
@@ -0,0 +1,214 @@
+/*
+ * Copyright © 2004-2020 L2J DataPack
+ * 
+ * This file is part of L2J DataPack.
+ * 
+ * L2J DataPack 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 DataPack 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 <http://www.gnu.org/licenses/>.
+ */
+package com.l2jserver.datapack.custom.service.base;
+
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.Map;
+import java.util.Objects;
+import java.util.concurrent.ConcurrentHashMap;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.l2jserver.datapack.ai.npc.AbstractNpcAI;
+import com.l2jserver.datapack.custom.service.base.model.entity.CustomServiceProduct;
+import com.l2jserver.datapack.custom.service.base.model.entity.CustomServiceServer;
+import com.l2jserver.datapack.custom.service.base.model.entity.ItemRequirement;
+import com.l2jserver.datapack.custom.service.base.util.CommandProcessor;
+import com.l2jserver.datapack.custom.service.base.util.htmltmpls.HTMLTemplateParser;
+import com.l2jserver.datapack.custom.service.base.util.htmltmpls.HTMLTemplatePlaceholder;
+import com.l2jserver.datapack.custom.service.base.util.htmltmpls.funcs.ChildrenCountFunc;
+import com.l2jserver.datapack.custom.service.base.util.htmltmpls.funcs.ExistsFunc;
+import com.l2jserver.datapack.custom.service.base.util.htmltmpls.funcs.ForeachFunc;
+import com.l2jserver.datapack.custom.service.base.util.htmltmpls.funcs.IfChildrenFunc;
+import com.l2jserver.datapack.custom.service.base.util.htmltmpls.funcs.IfFunc;
+import com.l2jserver.datapack.custom.service.base.util.htmltmpls.funcs.IncludeFunc;
+import com.l2jserver.gameserver.model.actor.L2Character;
+import com.l2jserver.gameserver.model.actor.L2Npc;
+import com.l2jserver.gameserver.model.actor.instance.L2PcInstance;
+import com.l2jserver.gameserver.model.zone.ZoneId;
+import com.l2jserver.gameserver.network.serverpackets.NpcHtmlMessage;
+import com.l2jserver.gameserver.taskmanager.AttackStanceTaskManager;
+import com.l2jserver.gameserver.util.Util;
+
+/**
+ * Custom service abstract script.
+ * @author HorridoJoho
+ * @version 2.6.2.0
+ */
+public abstract class CustomServiceScript extends AbstractNpcAI {
+	
+	private static final Logger LOG = LoggerFactory.getLogger(CustomServiceScript.class);
+	
+	public static final String SCRIPT_COLLECTION = "service";
+	
+	private final String scriptName;
+	
+	private final Path scriptPath;
+	
+	private final Map<Integer, String> lastPlayerHtmls = new ConcurrentHashMap<>();
+	
+	public CustomServiceScript(String name) {
+		super(name, SCRIPT_COLLECTION);
+		
+		Objects.requireNonNull(name);
+		scriptName = name;
+		scriptPath = Paths.get(SCRIPT_COLLECTION, scriptName);
+	}
+	
+	private void setLastPlayerHtml(L2PcInstance player, String command) {
+		lastPlayerHtmls.put(player.getObjectId(), command);
+	}
+	
+	private void showLastPlayerHtml(L2PcInstance player, L2Npc npc) {
+		String lastHtmlCommand = lastPlayerHtmls.get(player.getObjectId());
+		if (lastHtmlCommand != null) {
+			executeHtmlCommand(player, npc, new CommandProcessor(lastHtmlCommand));
+		}
+	}
+	
+	private String generateAdvancedHtml(L2PcInstance player, CustomServiceServer service, String path, Map<String, HTMLTemplatePlaceholder> placeholders) {
+		final String htmlPath = "/data/" + scriptPath + "/html/" + service.getHtmlFolder() + "/" + path;
+		debug(player, htmlPath);
+		return HTMLTemplateParser.fromCache(htmlPath, player, placeholders, IncludeFunc.INSTANCE, IfFunc.INSTANCE, ForeachFunc.INSTANCE, ExistsFunc.INSTANCE, IfChildrenFunc.INSTANCE, ChildrenCountFunc.INSTANCE);
+	}
+	
+	protected final boolean isInsideAnyZoneOf(L2Character character, ZoneId first, ZoneId... more) {
+		if (character.isInsideZone(first)) {
+			return true;
+		}
+		
+		if (more != null) {
+			for (ZoneId zone : more) {
+				if (character.isInsideZone(zone)) {
+					return true;
+				}
+			}
+		}
+		return false;
+	}
+	
+	protected final void fillItemAmountMap(Map<Integer, Long> items, CustomServiceProduct product) {
+		for (ItemRequirement item : product.getItems()) {
+			Long amount = items.get(item.getItem().getId());
+			if (amount == null) {
+				amount = 0L;
+			}
+			items.put(item.getItem().getId(), amount + item.getItemAmount());
+		}
+	}
+	
+	protected final void showAdvancedHtml(L2PcInstance player, CustomServiceServer service, L2Npc npc, String path, Map<String, HTMLTemplatePlaceholder> placeholders) {
+		placeholders.put(service.getHtmlAccessorName(), service.getPlaceholder());
+		String html = generateAdvancedHtml(player, service, path, placeholders);
+		
+		debug(html);
+		
+		switch (service.getDialogType()) {
+			case NPC:
+				player.sendPacket(new NpcHtmlMessage(npc == null ? 0 : npc.getObjectId(), html));
+				break;
+			case COMMUNITY:
+				Util.sendCBHtml(player, html, npc == null ? 0 : npc.getObjectId());
+				break;
+		}
+	}
+	
+	@Override
+	public final String onFirstTalk(L2Npc npc, L2PcInstance player) {
+		executeCommand(player, npc, null);
+		return null;
+	}
+	
+	public final void debug(L2PcInstance player, String message) {
+		if (player.isGM() && isDebugEnabled()) {
+			player.sendMessage(scriptName + ": " + message);
+		}
+	}
+	
+	public final void debug(String message) {
+		if (isDebugEnabled()) {
+			LOG.info("Custom Service Debug:" + message);
+		}
+	}
+	
+	public final void executeCommand(L2PcInstance player, L2Npc npc, String commandString) {
+		if (isInsideAnyZoneOf(player, ZoneId.PVP, ZoneId.SIEGE, ZoneId.WATER, ZoneId.JAIL, ZoneId.DANGER_AREA)) {
+			player.sendMessage("The service cannot be used here.");
+			return;
+		} else if ((player.getEventStatus() != null) || (player.getBlockCheckerArena() != -1) || player.isOnEvent() || player.isInOlympiadMode()) {
+			player.sendMessage("The service cannot be used in events.");
+			return;
+		} else if (player.isInDuel() || (player.getPvpFlag() == 1)) {
+			player.sendMessage("The service cannot be used in duel or pvp.");
+			return;
+		} else if (AttackStanceTaskManager.getInstance().hasAttackStanceTask(player)) {
+			player.sendMessage("The service cannot be used while in combat.");
+			return;
+		}
+		
+		if ((commandString == null) || commandString.isEmpty()) {
+			commandString = "html main";
+		}
+		
+		debug(player, "--------------------");
+		debug(player, commandString);
+		
+		CommandProcessor command = new CommandProcessor(commandString);
+		
+		if (command.matchAndRemove("html ", "h ")) {
+			String playerCommand = command.getRemaining();
+			if (!executeHtmlCommand(player, npc, command)) {
+				setLastPlayerHtml(player, "main");
+			} else {
+				setLastPlayerHtml(player, playerCommand);
+			}
+		} else {
+			if (executeActionCommand(player, npc, command)) {
+				showLastPlayerHtml(player, npc);
+			}
+		}
+	}
+	
+	/**
+	 * Method for Html command processing. The default html command is "main".<br>
+	 * This also means, "main" must be implemented. The return value indicates if the user supplied html command should be saved as last html command.
+	 * @param player
+	 * @param npc
+	 * @param command
+	 * @return {@code true} save the html command as last html command, {@code false} don't save the html command as last html command
+	 */
+	protected abstract boolean executeHtmlCommand(L2PcInstance player, L2Npc npc, CommandProcessor command);
+	
+	/**
+	 * Method for action command processing. The return value indicates if the last saved player html command should be executed after this method.
+	 * @param player
+	 * @param npc
+	 * @param command
+	 * @return {@code true} execute last saved html command of the player, {@code false} don't execute last saved html command of the player
+	 */
+	protected abstract boolean executeActionCommand(L2PcInstance player, L2Npc npc, CommandProcessor command);
+	
+	/**
+	 * Method to determine if debugging is enabled.
+	 * @return {@code true} debugging is enabled, {@code false} debugging is disabled
+	 */
+	protected abstract boolean isDebugEnabled();
+}
diff --git a/src/main/java/com/l2jserver/datapack/custom/service/base/DialogType.java b/src/main/java/com/l2jserver/datapack/custom/service/base/DialogType.java
new file mode 100644
index 0000000000..3692ffaaab
--- /dev/null
+++ b/src/main/java/com/l2jserver/datapack/custom/service/base/DialogType.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright © 2004-2020 L2J DataPack
+ * 
+ * This file is part of L2J DataPack.
+ * 
+ * L2J DataPack 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 DataPack 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 <http://www.gnu.org/licenses/>.
+ */
+package com.l2jserver.datapack.custom.service.base;
+
+/**
+ * Dialog Type enum.
+ * @author HorridoJoho
+ * @version 2.6.2.0
+ */
+public enum DialogType {
+	NPC,
+	COMMUNITY
+}
diff --git a/src/main/java/com/l2jserver/datapack/custom/service/base/model/entity/CustomServiceProduct.java b/src/main/java/com/l2jserver/datapack/custom/service/base/model/entity/CustomServiceProduct.java
new file mode 100644
index 0000000000..7cbda9caca
--- /dev/null
+++ b/src/main/java/com/l2jserver/datapack/custom/service/base/model/entity/CustomServiceProduct.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright © 2004-2020 L2J DataPack
+ * 
+ * This file is part of L2J DataPack.
+ * 
+ * L2J DataPack 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 DataPack 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 <http://www.gnu.org/licenses/>.
+ */
+package com.l2jserver.datapack.custom.service.base.model.entity;
+
+import java.util.List;
+
+import com.l2jserver.datapack.custom.service.base.util.htmltmpls.HTMLTemplatePlaceholder;
+
+/**
+ * Custom Service Product.
+ * @author HorridoJoho
+ * @version 2.6.2.0
+ */
+public abstract class CustomServiceProduct extends Refable {
+	private List<ItemRequirement> items;
+	
+	protected CustomServiceProduct() {
+	}
+	
+	@Override
+	public void afterDeserialize() {
+		super.afterDeserialize();
+		
+		if (!items.isEmpty()) {
+			HTMLTemplatePlaceholder itemsPlaceholder = getPlaceholder().addChild("items", null).getChild("items");
+			for (ItemRequirement item : items) {
+				itemsPlaceholder.addAliasChild(String.valueOf(itemsPlaceholder.getChildrenSize()), item.getPlaceholder());
+			}
+		}
+	}
+	
+	public final List<ItemRequirement> getItems() {
+		return items;
+	}
+}
diff --git a/src/main/java/com/l2jserver/datapack/custom/service/base/model/entity/CustomServiceServer.java b/src/main/java/com/l2jserver/datapack/custom/service/base/model/entity/CustomServiceServer.java
new file mode 100644
index 0000000000..0585eb9c6b
--- /dev/null
+++ b/src/main/java/com/l2jserver/datapack/custom/service/base/model/entity/CustomServiceServer.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright © 2004-2020 L2J DataPack
+ * 
+ * This file is part of L2J DataPack.
+ * 
+ * L2J DataPack 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 DataPack 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 <http://www.gnu.org/licenses/>.
+ */
+package com.l2jserver.datapack.custom.service.base.model.entity;
+
+import com.l2jserver.datapack.custom.service.base.DialogType;
+import com.l2jserver.datapack.custom.service.base.util.htmltmpls.HTMLTemplatePlaceholder;
+
+/**
+ * Custom Service Server.
+ * @author HorridoJoho
+ * @version 2.6.2.0
+ */
+public abstract class CustomServiceServer {
+	private DialogType dialogType;
+	private String htmlFolder;
+	
+	private final transient HTMLTemplatePlaceholder placeholder;
+	private final transient String bypassPrefix;
+	private final transient String htmlAccessorName;
+	
+	public CustomServiceServer(String bypassPrefix, String htmlAccessorName) {
+		dialogType = DialogType.NPC;
+		htmlFolder = null;
+		
+		placeholder = new HTMLTemplatePlaceholder("service", null);
+		this.bypassPrefix = "bypass -h " + bypassPrefix;
+		this.htmlAccessorName = htmlAccessorName;
+	}
+	
+	public void afterDeserialize() {
+		placeholder.addChild("bypass_prefix", bypassPrefix).addChild("name", getName());
+	}
+	
+	public final DialogType getDialogType() {
+		return dialogType;
+	}
+	
+	public final String getHtmlFolder() {
+		return htmlFolder;
+	}
+	
+	public final HTMLTemplatePlaceholder getPlaceholder() {
+		return placeholder;
+	}
+	
+	public final String getBypassPrefix() {
+		return bypassPrefix;
+	}
+	
+	public final String getHtmlAccessorName() {
+		return htmlAccessorName;
+	}
+	
+	public abstract String getName();
+}
diff --git a/src/main/java/com/l2jserver/datapack/custom/service/base/model/entity/IRefable.java b/src/main/java/com/l2jserver/datapack/custom/service/base/model/entity/IRefable.java
new file mode 100644
index 0000000000..d7bf47e79b
--- /dev/null
+++ b/src/main/java/com/l2jserver/datapack/custom/service/base/model/entity/IRefable.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright © 2004-2020 L2J DataPack
+ * 
+ * This file is part of L2J DataPack.
+ * 
+ * L2J DataPack 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 DataPack 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 <http://www.gnu.org/licenses/>.
+ */
+package com.l2jserver.datapack.custom.service.base.model.entity;
+
+/**
+ * Refable interface.
+ * @author HorridoJoho
+ * @param <T> id type
+ * @version 2.6.2.0
+ */
+public interface IRefable<T> {
+	T getId();
+}
diff --git a/src/main/java/com/l2jserver/datapack/custom/service/base/model/entity/ItemRequirement.java b/src/main/java/com/l2jserver/datapack/custom/service/base/model/entity/ItemRequirement.java
new file mode 100644
index 0000000000..f77907ec84
--- /dev/null
+++ b/src/main/java/com/l2jserver/datapack/custom/service/base/model/entity/ItemRequirement.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright © 2004-2020 L2J DataPack
+ * 
+ * This file is part of L2J DataPack.
+ * 
+ * L2J DataPack 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 DataPack 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 <http://www.gnu.org/licenses/>.
+ */
+package com.l2jserver.datapack.custom.service.base.model.entity;
+
+import com.l2jserver.datapack.custom.service.base.util.htmltmpls.HTMLTemplatePlaceholder;
+import com.l2jserver.gameserver.datatables.ItemTable;
+import com.l2jserver.gameserver.model.items.L2Item;
+
+/**
+ * Item Requirement.
+ * @author HorridoJoho
+ * @version 2.6.2.0
+ */
+public class ItemRequirement {
+	private int id;
+	private long amount;
+	
+	private final transient HTMLTemplatePlaceholder placeholder;
+
+	public ItemRequirement() {
+		id = 0;
+		amount = 0;
+		
+		placeholder = new HTMLTemplatePlaceholder("placeholder", null);
+	}
+	
+	public ItemRequirement(int id, long amount) {
+		this.id = id;
+		this.amount = amount;
+		
+		placeholder = new HTMLTemplatePlaceholder("placeholder", null);
+		
+		afterDeserialize();
+	}
+	
+	public void afterDeserialize() {
+		final L2Item item = getItem();
+		placeholder.addChild("id", String.valueOf(item.getId())).addChild("icon", item.getIcon()).addChild("name", item.getName()).addChild("amount", String.valueOf(amount));
+	}
+	
+	public final int getItemId() {
+		return id;
+	}
+	
+	public final long getItemAmount() {
+		return amount;
+	}
+	
+	public HTMLTemplatePlaceholder getPlaceholder() {
+		return placeholder;
+	}
+	
+	public final L2Item getItem() {
+		return ItemTable.getInstance().getTemplate(id);
+	}
+}
diff --git a/src/main/java/com/l2jserver/datapack/custom/service/base/model/entity/Refable.java b/src/main/java/com/l2jserver/datapack/custom/service/base/model/entity/Refable.java
new file mode 100644
index 0000000000..0bf85cc1f1
--- /dev/null
+++ b/src/main/java/com/l2jserver/datapack/custom/service/base/model/entity/Refable.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright © 2004-2020 L2J DataPack
+ * 
+ * This file is part of L2J DataPack.
+ * 
+ * L2J DataPack 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 DataPack 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 <http://www.gnu.org/licenses/>.
+ */
+package com.l2jserver.datapack.custom.service.base.model.entity;
+
+import java.util.Objects;
+
+import com.l2jserver.datapack.custom.service.base.util.htmltmpls.HTMLTemplatePlaceholder;
+
+/**
+ * Refable.
+ * @author HorridoJoho
+ * @version 2.6.2.0
+ */
+public abstract class Refable implements IRefable<String> {
+	private String id;
+	
+	private final transient HTMLTemplatePlaceholder placeholder;
+	
+	protected Refable() {
+		id = null;
+		
+		placeholder = new HTMLTemplatePlaceholder("placeholder", null);
+	}
+	
+	protected Refable(String id) {
+		Objects.requireNonNull(id);
+		this.id = id;
+		
+		placeholder = new HTMLTemplatePlaceholder("placeholder", null);
+	}
+	
+	public void afterDeserialize() {
+		placeholder.addChild("ident", id);
+	}
+	
+	@Override
+	public final String getId() {
+		return id;
+	}
+	
+	public final HTMLTemplatePlaceholder getPlaceholder() {
+		return placeholder;
+	}
+}
diff --git a/src/main/java/com/l2jserver/datapack/custom/service/base/util/CommandProcessor.java b/src/main/java/com/l2jserver/datapack/custom/service/base/util/CommandProcessor.java
new file mode 100644
index 0000000000..2212301132
--- /dev/null
+++ b/src/main/java/com/l2jserver/datapack/custom/service/base/util/CommandProcessor.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright © 2004-2020 L2J DataPack
+ * 
+ * This file is part of L2J DataPack.
+ * 
+ * L2J DataPack 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 DataPack 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 <http://www.gnu.org/licenses/>.
+ */
+package com.l2jserver.datapack.custom.service.base.util;
+
+import java.util.Objects;
+
+/**
+ * Command processor.
+ * @author HorridoJoho
+ * @version 2.6.2.0
+ */
+public final class CommandProcessor {
+	private String remaining;
+	
+	public CommandProcessor(String command) {
+		Objects.requireNonNull(command);
+		remaining = command;
+	}
+	
+	public boolean matchAndRemove(String... expectations) {
+		Objects.requireNonNull(expectations);
+		for (String expectation : expectations) {
+			Objects.requireNonNull(expectation);
+			if (!expectation.isEmpty() && remaining.startsWith(expectation)) {
+				remaining = remaining.substring(expectation.length());
+				return true;
+			}
+		}
+		return false;
+	}
+	
+	public String[] splitRemaining(String regex) {
+		return remaining.split(regex);
+	}
+	
+	public String getRemaining() {
+		return remaining;
+	}
+}
diff --git a/src/main/java/com/l2jserver/datapack/custom/service/base/util/htmltmpls/HTMLTemplateFunc.java b/src/main/java/com/l2jserver/datapack/custom/service/base/util/htmltmpls/HTMLTemplateFunc.java
new file mode 100644
index 0000000000..dc1a035fac
--- /dev/null
+++ b/src/main/java/com/l2jserver/datapack/custom/service/base/util/htmltmpls/HTMLTemplateFunc.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright © 2004-2020 L2J DataPack
+ * 
+ * This file is part of L2J DataPack.
+ * 
+ * L2J DataPack 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 DataPack 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 <http://www.gnu.org/licenses/>.
+ */
+package com.l2jserver.datapack.custom.service.base.util.htmltmpls;
+
+import java.util.Map;
+
+import com.l2jserver.gameserver.model.actor.instance.L2PcInstance;
+
+/**
+ * This class represents a template function.<br>
+ * It has a sequence start and end.<br>
+ * ----- Example -----<br>
+ * -- startSequence = "INC"<br>
+ * -- endSequence = "ENDINC"<br>
+ * We have a template file template.tmpl: [INC(template2.tmpl)ENDINC]<br>
+ * Now when the handlers {@link #handle(StringBuilder, L2PcInstance, Map, HTMLTemplateFunc[])} method is called, contents will contain "template2.tmpl"<br>
+ * @author HorridoJoho
+ * @version 2.6.2.0
+ */
+public abstract class HTMLTemplateFunc {
+	/** how the function sequence starts */
+	private final String sequenceStart;
+	/** how the function sequence ends */
+	private final String sequenceEnd;
+	/** flag to determine if the template function needs processing of it's contents before contents are passed to the {@link #handle(StringBuilder, L2PcInstance, Map, HTMLTemplateFunc[])} method */
+	private final boolean requiresPreprocessing;
+	
+	/**
+	 * Protected constructor for template function implementations. In a template document
+	 * @param sequenceStart how the template function sequence starts (in a template document you use [sequenceStart(
+	 * @param sequenceEnd how the template function sequence starts (in a template document you use )sequenceEnd]
+	 * @param requiresPreprocessing flag to determine if the template function needs processing of it's contents before contents are passed to the {@link #handle(StringBuilder, L2PcInstance, Map, HTMLTemplateFunc[])}
+	 */
+	protected HTMLTemplateFunc(String sequenceStart, String sequenceEnd, boolean requiresPreprocessing) {
+		this.sequenceStart = "[" + sequenceStart + "(";
+		this.sequenceEnd = ")" + sequenceEnd + "]";
+		this.requiresPreprocessing = requiresPreprocessing;
+	}
+	
+	/**
+	 * @return the sequence this function starts with in a template document
+	 */
+	public final String getSequenceStart() {
+		return sequenceStart;
+	}
+	
+	/**
+	 * @return the sequence this function ends with in a template document
+	 */
+	public final String getSequenceEnd() {
+		return sequenceEnd;
+	}
+	
+	/**
+	 * @return true when the handler needs the contents pre-processed by the template engine before it is passed to the {@link #handle(StringBuilder, L2PcInstance, Map, HTMLTemplateFunc[])} method, false otherwise
+	 */
+	public final boolean requiresPreprocessing() {
+		return requiresPreprocessing;
+	}
+	
+	/**
+	 * Called by template parser to give the function the possibility to<br>
+	 * create new placeholders and modify the contents of the function<br>
+	 * in the template document.
+	 * @param content the content which can be modified by the handler
+	 * @param player the player the template is processed for
+	 * @param placeholders the currently available placeholders as unmodifiable map
+	 * @param funcs supported functions the template is parsed with
+	 * @return placeholder to add to the currently available placeholders, added before the content is processed after this call, and removed again after content processing
+	 */
+	public abstract Map<String, HTMLTemplatePlaceholder> handle(StringBuilder content, L2PcInstance player, Map<String, HTMLTemplatePlaceholder> placeholders, HTMLTemplateFunc[] funcs);
+}
\ No newline at end of file
diff --git a/src/main/java/com/l2jserver/datapack/custom/service/base/util/htmltmpls/HTMLTemplateParser.java b/src/main/java/com/l2jserver/datapack/custom/service/base/util/htmltmpls/HTMLTemplateParser.java
new file mode 100644
index 0000000000..c6ffc9ad7d
--- /dev/null
+++ b/src/main/java/com/l2jserver/datapack/custom/service/base/util/htmltmpls/HTMLTemplateParser.java
@@ -0,0 +1,154 @@
+/*
+ * Copyright © 2004-2020 L2J DataPack
+ * 
+ * This file is part of L2J DataPack.
+ * 
+ * L2J DataPack 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 DataPack 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 <http://www.gnu.org/licenses/>.
+ */
+package com.l2jserver.datapack.custom.service.base.util.htmltmpls;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import com.l2jserver.gameserver.cache.HtmCache;
+import com.l2jserver.gameserver.model.actor.instance.L2PcInstance;
+
+/**
+ * HTML Template Parser.
+ * @author HorridoJoho
+ * @version 2.6.2.0
+ */
+public final class HTMLTemplateParser {
+	/** pattern to find placeholder references */
+	private static final Pattern PLACEHOLDER_PATTERN = Pattern.compile("%[a-zA-Z_][a-zA-Z0-9_]*(\\.[a-zA-Z_][a-zA-Z0-9_]*)*%");
+	
+	public static String fromCache(String path, Map<String, HTMLTemplatePlaceholder> placeholders, HTMLTemplateFunc... funcs) {
+		return fromCache(path, null, placeholders, funcs);
+	}
+	
+	public static String fromCache(String path, L2PcInstance player, Map<String, HTMLTemplatePlaceholder> placeholders, HTMLTemplateFunc... funcs) {
+		String string = HtmCache.getInstance().getHtm(player.getHtmlPrefix(), path);
+		if (string == null) {
+			return null;
+		}
+		StringBuilder builder = new StringBuilder(string);
+		fromStringBuilder(builder, player, placeholders, funcs);
+		return builder.toString();
+	}
+	
+	/**
+	 * Method to process a template. The string is directly modified and will contain the results of the template processing.
+	 * @param string the template content
+	 * @param player the player the template is processed for
+	 * @param placeholders a map of placeholder (map has to be modifiable)
+	 * @param funcs the functions to use while processing the template
+	 */
+	public static void fromStringBuilder(StringBuilder string, L2PcInstance player, Map<String, HTMLTemplatePlaceholder> placeholders, HTMLTemplateFunc... funcs) {
+		if (string == null) {
+			return;
+		}
+		
+		int indexOfOffset = 0;
+		
+		while (indexOfOffset < (string.length() - 1)) {
+			// find the first position of a placeholder or a custom func
+			Matcher placeholderMatcher = PLACEHOLDER_PATTERN.matcher(string);
+			int nextFuncStartOffset = -1;
+			int nextFuncEndOffset = -1;
+			if (placeholderMatcher.find(indexOfOffset)) {
+				nextFuncStartOffset = placeholderMatcher.start();
+				nextFuncEndOffset = placeholderMatcher.end();
+			}
+			
+			HTMLTemplateFunc nextFunc = null;
+			for (HTMLTemplateFunc func : funcs) {
+				int funcOffset = string.indexOf(func.getSequenceStart(), indexOfOffset);
+				if ((funcOffset > -1) && ((nextFuncStartOffset == -1) || (funcOffset < nextFuncStartOffset))) {
+					nextFuncStartOffset = funcOffset;
+					nextFuncEndOffset = HTMLTemplateUtils.findSequenceEnd(string, nextFuncStartOffset + func.getSequenceStart().length(), func);
+					nextFunc = func;
+				}
+			}
+			
+			if (nextFuncStartOffset == -1) {
+				break;
+			} else if (nextFunc == null) {
+				String placeholderString = placeholderMatcher.group().substring(1, placeholderMatcher.group().length() - 1);
+				HTMLTemplatePlaceholder placeholder = HTMLTemplateUtils.getPlaceholder(placeholderString, placeholders);
+				
+				if (placeholder != null) {
+					string.replace(nextFuncStartOffset, nextFuncEndOffset, placeholder.getValue());
+					// 2 !!! placeholder replacement can contain more placeholders and func sequences start so we set the index to search to the start of the placeholder
+					indexOfOffset = nextFuncStartOffset;
+				} else
+				// skip placeholder?
+				{
+					// if placeholder can not be found, just remove it from the string
+					// 1 <<< string.delete(nextFuncStartOffset, nextFuncEndOffset);
+					
+					// 2 !!! l2j compatible mode, we don't want to manually add things like %objectId% placeholders all the time
+					indexOfOffset += placeholderMatcher.end() - placeholderMatcher.start();
+				}
+				
+				// placeholder replacement can contain more placeholders and func sequences start so we set the index to search to the start of the placeholder
+				// 1 <<< indexOfOffset = nextFuncStartOffset;
+			} else {
+				if (nextFuncEndOffset == -1) {
+					// this is to ignore the starting sequences which have no ending sequence
+					++indexOfOffset;
+				} else {
+					StringBuilder content = new StringBuilder(string.subSequence(nextFuncStartOffset + nextFunc.getSequenceStart().length(), nextFuncEndOffset - nextFunc.getSequenceEnd().length()));
+					
+					// the func needs pre-processing?
+					if (nextFunc.requiresPreprocessing()) {
+						fromStringBuilder(content, player, placeholders, funcs);
+					}
+					
+					Map<String, HTMLTemplatePlaceholder> tmpPlaceholders = nextFunc.handle(content, player, placeholders == null ? null : Collections.unmodifiableMap(placeholders), funcs);
+					
+					// add new entries and replace entries(temp)
+					if (tmpPlaceholders != null) {
+						for (HTMLTemplatePlaceholder newPlaceholder : tmpPlaceholders.values()) {
+							if (placeholders == null) {
+								placeholders = new HashMap<>();
+							}
+							tmpPlaceholders.put(newPlaceholder.getName(), placeholders.put(newPlaceholder.getName(), newPlaceholder));
+						}
+					}
+					
+					fromStringBuilder(content, player, placeholders, funcs);
+					string.replace(nextFuncStartOffset, nextFuncEndOffset, content.toString());
+					
+					// remove entries which were new and restore old entries
+					if ((tmpPlaceholders != null) && (placeholders != null)) {
+						for (Entry<String, HTMLTemplatePlaceholder> oldPlaceholder : tmpPlaceholders.entrySet()) {
+							if (oldPlaceholder.getValue() == null) {
+								placeholders.remove(oldPlaceholder.getKey());
+							} else {
+								placeholders.put(oldPlaceholder.getKey(), oldPlaceholder.getValue());
+							}
+						}
+					}
+					
+					// set the current offset to the next func sequence start found, replaced content can contain more placeholders and funcs
+					indexOfOffset = nextFuncStartOffset;
+				}
+			}
+		}
+	}
+}
\ No newline at end of file
diff --git a/src/main/java/com/l2jserver/datapack/custom/service/base/util/htmltmpls/HTMLTemplatePlaceholder.java b/src/main/java/com/l2jserver/datapack/custom/service/base/util/htmltmpls/HTMLTemplatePlaceholder.java
new file mode 100644
index 0000000000..d513cb7793
--- /dev/null
+++ b/src/main/java/com/l2jserver/datapack/custom/service/base/util/htmltmpls/HTMLTemplatePlaceholder.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright © 2004-2020 L2J DataPack
+ * 
+ * This file is part of L2J DataPack.
+ * 
+ * L2J DataPack 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 DataPack 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 <http://www.gnu.org/licenses/>.
+ */
+package com.l2jserver.datapack.custom.service.base.util.htmltmpls;
+
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+/**
+ * This is the class for the built-in value placeholder.<br>
+ * It has a name, a value and can contain child placeholder.<br>
+ * To reference the value of a placeholder in a template document<br>
+ * you use <b>%placeholder_name%</b>. To reference the value of a child<br>
+ * placeholder you use <b>%placeholder_name.child_placeholder_name%.</b>
+ * @author HorridoJoho
+ * @version 2.6.2.0
+ */
+public final class HTMLTemplatePlaceholder {
+	/** the name of this placeholder */
+	private final String name;
+	/** the value of this placeholder */
+	private volatile String value;
+	/** the child placeholders of this placeholder */
+	private final Map<String, HTMLTemplatePlaceholder> children;
+	
+	/**
+	 * Public constructor to create a new placeholder
+	 * @param name the name of the new placeholder
+	 * @param value the value of the new placeholder
+	 */
+	public HTMLTemplatePlaceholder(String name, String value) {
+		this(name, value, new LinkedHashMap<>());
+	}
+	
+	/**
+	 * Private constructor to create alias placeholders of other placeholders
+	 * @param name the name of the alias placeholder
+	 * @param value the value of the alias placeholder
+	 * @param children the children of the alias placeholder
+	 */
+	private HTMLTemplatePlaceholder(String name, String value, Map<String, HTMLTemplatePlaceholder> children) {
+		this.name = name;
+		this.value = value;
+		this.children = children;
+	}
+	
+	/**
+	 * Creates an alias for this placeholder.<br>
+	 * An alias placeholder will hold the reference to the children map from the original placeholder. This means, adding a new child to the alias will also add the child to the original placeholder and vice versa.
+	 * @param name name of the alias placeholder
+	 * @return the newly created alias placeholder
+	 */
+	public HTMLTemplatePlaceholder createAlias(String name) {
+		return new HTMLTemplatePlaceholder(name, value, children);
+	}
+	
+	/**
+	 * Adds a child placeholder to this placeholder.
+	 * @param name the name of the new child placeholder
+	 * @param value the value of the new child placeholder
+	 * @return this placeholder
+	 */
+	public HTMLTemplatePlaceholder addChild(String name, String value) {
+		children.put(name, new HTMLTemplatePlaceholder(name, value));
+		return this;
+	}
+	
+	public HTMLTemplatePlaceholder addAliasChild(String aliasName, HTMLTemplatePlaceholder placeholder) {
+		children.put(aliasName, placeholder.createAlias(aliasName));
+		return this;
+	}
+	
+	public void setValue(String value) {
+		this.value = value;
+	}
+	
+	public String getName() {
+		return name;
+	}
+	
+	public String getValue() {
+		return value;
+	}
+	
+	/**
+	 * Method to get a child placeholder of this placeholder by name
+	 * @param name the name of the child placeholder to find
+	 * @return the child placeholder
+	 */
+	public HTMLTemplatePlaceholder getChild(String name) {
+		return HTMLTemplateUtils.getPlaceholder(name, children);
+	}
+	
+	/**
+	 * @return the child placeholder map of this placeholder as unmodifiable map
+	 */
+	public Map<String, HTMLTemplatePlaceholder> getChildren() {
+		return Collections.unmodifiableMap(children);
+	}
+	
+	/**
+	 * @return the count of child placeholders in this placeholder
+	 */
+	public int getChildrenSize() {
+		return children.size();
+	}
+}
\ No newline at end of file
diff --git a/src/main/java/com/l2jserver/datapack/custom/service/base/util/htmltmpls/HTMLTemplateUtils.java b/src/main/java/com/l2jserver/datapack/custom/service/base/util/htmltmpls/HTMLTemplateUtils.java
new file mode 100644
index 0000000000..506134ab49
--- /dev/null
+++ b/src/main/java/com/l2jserver/datapack/custom/service/base/util/htmltmpls/HTMLTemplateUtils.java
@@ -0,0 +1,129 @@
+/*
+ * Copyright © 2004-2020 L2J DataPack
+ * 
+ * This file is part of L2J DataPack.
+ * 
+ * L2J DataPack 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 DataPack 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 <http://www.gnu.org/licenses/>.
+ */
+package com.l2jserver.datapack.custom.service.base.util.htmltmpls;
+
+import java.util.Map;
+import java.util.regex.Pattern;
+
+/**
+ * HTML Template Utils.
+ * @author HorridoJoho
+ * @version 2.6.2.0
+ */
+public final class HTMLTemplateUtils {
+	public static int findSequenceEnd(StringBuilder string, int startOffset, HTMLTemplateFunc func) {
+		int dept = 0;
+		String seqStart = func.getSequenceStart();
+		String seqEnd = func.getSequenceEnd();
+		String escapedSeqStart = "\\" + seqStart;
+		String escapedSeqEnd = "\\" + seqEnd;
+		
+		while (true) {
+			int endSeqOffset = string.indexOf(seqEnd, startOffset);
+			if (endSeqOffset == -1) {
+				return -1; // there is no sequence end to find
+			}
+			
+			int escapedSeqStartOffset = string.indexOf(escapedSeqStart, startOffset);
+			int escapedSeqEndOffset = string.indexOf(escapedSeqEnd, startOffset);
+			int startSeqOffset = string.indexOf(seqStart, startOffset);
+			
+			if (((endSeqOffset < startSeqOffset) || (startSeqOffset == -1)) && ((endSeqOffset < escapedSeqStartOffset) || (escapedSeqStartOffset == -1)) && ((endSeqOffset < escapedSeqEndOffset) || (escapedSeqEndOffset == -1))) {
+				if (dept == 0) {
+					return endSeqOffset + seqEnd.length();
+				}
+				
+				--dept;
+				startOffset = endSeqOffset + seqEnd.length();
+			} else if ((startSeqOffset != -1) && (startSeqOffset < endSeqOffset) && ((startSeqOffset < escapedSeqStartOffset) || (escapedSeqStartOffset == -1)) && ((startSeqOffset < escapedSeqEndOffset) || (escapedSeqEndOffset == -1))) {
+				startOffset = startSeqOffset + seqStart.length();
+				++dept;
+			} else if ((escapedSeqStartOffset != -1) && ((escapedSeqStartOffset < escapedSeqEndOffset) || (escapedSeqEndOffset == -1))) {
+				startOffset += escapedSeqStartOffset + escapedSeqStart.length();
+			} else if ((escapedSeqEndOffset != -1) && ((escapedSeqEndOffset < escapedSeqStartOffset) || (escapedSeqStartOffset != -1))) {
+				startOffset += escapedSeqEndOffset + escapedSeqEnd.length();
+			}
+		}
+	}
+	
+	/**
+	 * Searches the placeholder specified by placeholderString inside the placeholders map
+	 * @param placeholderString placeholder to search for
+	 * @param placeholders map with the placeholders available
+	 * @return the placeholder if found, null otherwise
+	 */
+	public static HTMLTemplatePlaceholder getPlaceholder(String placeholderString, Map<String, HTMLTemplatePlaceholder> placeholders) {
+		if (placeholders == null) {
+			return null;
+		}
+		
+		String[] placeholderParts = placeholderString.split(Pattern.quote("."));
+		HTMLTemplatePlaceholder placeholder = null;
+		for (String placeholderPart : placeholderParts) {
+			if (placeholder == null) {
+				placeholder = placeholders.get(placeholderPart);
+				if (placeholder == null) {
+					break;
+				}
+			} else {
+				placeholder = placeholder.getChild(placeholderPart);
+				if (placeholder == null) {
+					break;
+				}
+			}
+		}
+		return placeholder;
+	}
+	
+	/**
+	 * Get the value of the placeholder specified by placeholderString
+	 * @param placeholderString the placeholder to get the value from
+	 * @param placeholders placeholder map to search in
+	 * @return the value of the found placeholder
+	 * @throws Exception the placeholder was not found or the value is null
+	 */
+	public static String getPlaceholderValue(String placeholderString, Map<String, HTMLTemplatePlaceholder> placeholders) throws Exception {
+		HTMLTemplatePlaceholder placeholder = getPlaceholder(placeholderString, placeholders);
+		if (placeholder == null) {
+			throw new Exception();
+		}
+		
+		String value = placeholder.getValue();
+		if (value == null) {
+			throw new Exception();
+		}
+		
+		return value;
+	}
+	
+	/**
+	 * Get the childs of the placeholder specified by placeholderString in an unmodifyable map
+	 * @param placeholderString the placeholder to get the childs from
+	 * @param placeholders placeholder map to search in
+	 * @return the childs in an unmodifyable map of the found placeholder
+	 * @throws Exception the placeholder was not found
+	 */
+	public static Map<String, HTMLTemplatePlaceholder> getPlaceholderChilds(String placeholderString, Map<String, HTMLTemplatePlaceholder> placeholders) throws Exception {
+		HTMLTemplatePlaceholder placeholder = getPlaceholder(placeholderString, placeholders);
+		if (placeholder == null) {
+			throw new Exception();
+		}
+		return placeholder.getChildren();
+	}
+}
diff --git a/src/main/java/com/l2jserver/datapack/custom/service/base/util/htmltmpls/funcs/ChildrenCountFunc.java b/src/main/java/com/l2jserver/datapack/custom/service/base/util/htmltmpls/funcs/ChildrenCountFunc.java
new file mode 100644
index 0000000000..eae710443f
--- /dev/null
+++ b/src/main/java/com/l2jserver/datapack/custom/service/base/util/htmltmpls/funcs/ChildrenCountFunc.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright © 2004-2020 L2J DataPack
+ * 
+ * This file is part of L2J DataPack.
+ * 
+ * L2J DataPack 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 DataPack 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 <http://www.gnu.org/licenses/>.
+ */
+package com.l2jserver.datapack.custom.service.base.util.htmltmpls.funcs;
+
+import java.util.Map;
+
+import com.l2jserver.datapack.custom.service.base.util.htmltmpls.HTMLTemplateFunc;
+import com.l2jserver.datapack.custom.service.base.util.htmltmpls.HTMLTemplatePlaceholder;
+import com.l2jserver.datapack.custom.service.base.util.htmltmpls.HTMLTemplateUtils;
+import com.l2jserver.gameserver.model.actor.instance.L2PcInstance;
+
+/**
+ * Children Count function.
+ * @author HorridoJoho
+ * @version 2.6.2.0
+ */
+public final class ChildrenCountFunc extends HTMLTemplateFunc {
+	public static final ChildrenCountFunc INSTANCE = new ChildrenCountFunc();
+	
+	private ChildrenCountFunc() {
+		super("CHILDSCOUNT", "ENDCHILDSCOUNT", false);
+	}
+	
+	@Override
+	public Map<String, HTMLTemplatePlaceholder> handle(StringBuilder content, L2PcInstance player, Map<String, HTMLTemplatePlaceholder> placeholders, HTMLTemplateFunc[] funcs) {
+		HTMLTemplatePlaceholder placeholder = HTMLTemplateUtils.getPlaceholder(content.toString(), placeholders);
+		content.setLength(0);
+		if (placeholder != null) {
+			content.append(placeholder.getChildrenSize());
+		}
+		return null;
+	}
+}
diff --git a/src/main/java/com/l2jserver/datapack/custom/service/base/util/htmltmpls/funcs/ExistsFunc.java b/src/main/java/com/l2jserver/datapack/custom/service/base/util/htmltmpls/funcs/ExistsFunc.java
new file mode 100644
index 0000000000..25b4dca896
--- /dev/null
+++ b/src/main/java/com/l2jserver/datapack/custom/service/base/util/htmltmpls/funcs/ExistsFunc.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright © 2004-2020 L2J DataPack
+ * 
+ * This file is part of L2J DataPack.
+ * 
+ * L2J DataPack 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 DataPack 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 <http://www.gnu.org/licenses/>.
+ */
+package com.l2jserver.datapack.custom.service.base.util.htmltmpls.funcs;
+
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import com.l2jserver.datapack.custom.service.base.util.htmltmpls.HTMLTemplateFunc;
+import com.l2jserver.datapack.custom.service.base.util.htmltmpls.HTMLTemplatePlaceholder;
+import com.l2jserver.datapack.custom.service.base.util.htmltmpls.HTMLTemplateUtils;
+import com.l2jserver.gameserver.model.actor.instance.L2PcInstance;
+
+/**
+ * Exists function.
+ * @author HorridoJoho
+ * @version 2.6.2.0
+ */
+public final class ExistsFunc extends HTMLTemplateFunc {
+	public static final ExistsFunc INSTANCE = new ExistsFunc();
+	
+	private static final Pattern NEGATE_PATTERN = Pattern.compile("\\s*!\\s*");
+	private static final Pattern PLACEHOLDER_PATTERN = Pattern.compile("[a-zA-Z_][a-zA-Z0-9_]*(\\.[a-zA-Z_][a-zA-Z0-9_]*)*\\s*,");
+	
+	private ExistsFunc() {
+		super("EXISTS", "ENDEXISTS", false);
+	}
+	
+	@Override
+	public Map<String, HTMLTemplatePlaceholder> handle(StringBuilder content, L2PcInstance player, Map<String, HTMLTemplatePlaceholder> placeholders, HTMLTemplateFunc[] funcs) {
+		try {
+			boolean negate = false;
+			Matcher m = null;
+			
+			try {
+				m = getMatcher(NEGATE_PATTERN, content, 0);
+				negate = true;
+			} catch (Exception e) {
+				// ignore this exception, negate is optional
+			}
+			
+			if (m != null) {
+				m = getMatcher(PLACEHOLDER_PATTERN, content, m.end());
+			} else {
+				m = getMatcher(PLACEHOLDER_PATTERN, content, 0);
+			}
+			
+			HTMLTemplatePlaceholder placeholder = HTMLTemplateUtils.getPlaceholder(m.group().substring(0, m.group().length() - 1).trim(), placeholders);
+			if (((placeholder == null) && !negate) || ((placeholder != null) && negate)) {
+				content.setLength(0);
+				return null;
+			}
+			
+			content.delete(0, m.end());
+		} catch (Exception e) {
+			content.setLength(0);
+		}
+		return null;
+	}
+	
+	private static Matcher getMatcher(Pattern pattern, StringBuilder content, int findIndex) throws Exception {
+		Matcher m = pattern.matcher(content);
+		if (!m.find(findIndex) || (m.start() > findIndex)) {
+			throw new Exception();
+		}
+		
+		return m;
+	}
+}
diff --git a/src/main/java/com/l2jserver/datapack/custom/service/base/util/htmltmpls/funcs/ForeachFunc.java b/src/main/java/com/l2jserver/datapack/custom/service/base/util/htmltmpls/funcs/ForeachFunc.java
new file mode 100644
index 0000000000..8fda839d28
--- /dev/null
+++ b/src/main/java/com/l2jserver/datapack/custom/service/base/util/htmltmpls/funcs/ForeachFunc.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright © 2004-2020 L2J DataPack
+ * 
+ * This file is part of L2J DataPack.
+ * 
+ * L2J DataPack 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 DataPack 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 <http://www.gnu.org/licenses/>.
+ */
+package com.l2jserver.datapack.custom.service.base.util.htmltmpls.funcs;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import com.l2jserver.datapack.custom.service.base.util.htmltmpls.HTMLTemplateFunc;
+import com.l2jserver.datapack.custom.service.base.util.htmltmpls.HTMLTemplateParser;
+import com.l2jserver.datapack.custom.service.base.util.htmltmpls.HTMLTemplatePlaceholder;
+import com.l2jserver.datapack.custom.service.base.util.htmltmpls.HTMLTemplateUtils;
+import com.l2jserver.gameserver.model.actor.instance.L2PcInstance;
+
+/**
+ * This class implements the following function syntax:<br>
+ * [FOREACH(alias_placeholder_name IN placeholder_name DO text per iteration)ENDEACH]<br>
+ * <br>
+ * This construct is able to iterate over the children of the "placeholder_name" placeholder.<br>
+ * For each child in this placeholder, the text after "DO" is placed in the content.<br>
+ * The current child is placed as an alias toplevel placeholder. This means, in this example<br>
+ * you can use %alias_placeholder_name% inside the foreach block.
+ * @author HorridoJoho
+ * @version 2.6.2.0
+ */
+public final class ForeachFunc extends HTMLTemplateFunc {
+	public static final ForeachFunc INSTANCE = new ForeachFunc();
+	
+	private static final Pattern FIRST_PLACEHOLDER_PATTERN = Pattern.compile("\\s*[a-zA-Z_][a-zA-Z0-9_]*(\\.[a-zA-Z_][a-zA-Z0-9_]*)*");
+	
+	private static final Pattern IN_PATTERN = Pattern.compile("\\s*\\sIN\\s");
+	
+	private static final Pattern SECOND_PLACEHOLDER_PATTERN = Pattern.compile("\\s*[a-zA-Z_][a-zA-Z0-9_]*(\\.[a-zA-Z_][a-zA-Z0-9_]*)*");
+	
+	private static final Pattern DO_PATTERN = Pattern.compile("\\s*\\sDO\\s");
+	
+	private ForeachFunc() {
+		super("FOREACH", "ENDEACH", false);
+	}
+	
+	@Override
+	public Map<String, HTMLTemplatePlaceholder> handle(StringBuilder content, L2PcInstance player, Map<String, HTMLTemplatePlaceholder> placeholders, HTMLTemplateFunc[] funcs) {
+		try {
+			Matcher matcher = getMatcher(FIRST_PLACEHOLDER_PATTERN, content, 0);
+			String aliasPlaceholderName = matcher.group().trim();
+			int findIndex = matcher.end();
+			
+			matcher = getMatcher(IN_PATTERN, content, findIndex);
+			findIndex = matcher.end();
+			
+			matcher = getMatcher(SECOND_PLACEHOLDER_PATTERN, content, findIndex);
+			Map<String, HTMLTemplatePlaceholder> childPlaceholders = HTMLTemplateUtils.getPlaceholderChilds(matcher.group().trim(), placeholders);
+			findIndex = matcher.end();
+			
+			matcher = getMatcher(DO_PATTERN, content, findIndex);
+			findIndex = matcher.end();
+			
+			content.delete(0, findIndex);
+			HashMap<String, HTMLTemplatePlaceholder> newPlaceholders = new HashMap<>(placeholders);
+			StringBuilder orgContent = new StringBuilder(content);
+			StringBuilder modContent = new StringBuilder(content.length());
+			content.setLength(0);
+			// we don't need to save an overwritten placeholder, we create our own map
+			for (Entry<String, HTMLTemplatePlaceholder> childPlaceholder : childPlaceholders.entrySet()) {
+				modContent.setLength(0);
+				modContent.append(orgContent);
+				newPlaceholders.put(aliasPlaceholderName, childPlaceholder.getValue().createAlias(aliasPlaceholderName));
+				HTMLTemplateParser.fromStringBuilder(modContent, player, newPlaceholders, funcs);
+				content.append(modContent);
+			}
+		} catch (Exception ex) {
+			content.setLength(0);
+		}
+		return null;
+	}
+	
+	private static Matcher getMatcher(Pattern pattern, StringBuilder content, int findIndex) throws Exception {
+		Matcher m = pattern.matcher(content);
+		if (!m.find(findIndex) || (m.start() > findIndex)) {
+			throw new Exception();
+		}
+		return m;
+	}
+}
diff --git a/src/main/java/com/l2jserver/datapack/custom/service/base/util/htmltmpls/funcs/IfChildrenFunc.java b/src/main/java/com/l2jserver/datapack/custom/service/base/util/htmltmpls/funcs/IfChildrenFunc.java
new file mode 100644
index 0000000000..c63c491863
--- /dev/null
+++ b/src/main/java/com/l2jserver/datapack/custom/service/base/util/htmltmpls/funcs/IfChildrenFunc.java
@@ -0,0 +1,144 @@
+/*
+ * Copyright © 2004-2020 L2J DataPack
+ * 
+ * This file is part of L2J DataPack.
+ * 
+ * L2J DataPack 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 DataPack 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 <http://www.gnu.org/licenses/>.
+ */
+package com.l2jserver.datapack.custom.service.base.util.htmltmpls.funcs;
+
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import com.l2jserver.datapack.custom.service.base.util.htmltmpls.HTMLTemplateFunc;
+import com.l2jserver.datapack.custom.service.base.util.htmltmpls.HTMLTemplatePlaceholder;
+import com.l2jserver.datapack.custom.service.base.util.htmltmpls.HTMLTemplateUtils;
+import com.l2jserver.gameserver.model.actor.instance.L2PcInstance;
+
+/**
+ * If Children function.
+ * @author HorridoJoho
+ * @version 2.6.2.0
+ */
+public final class IfChildrenFunc extends HTMLTemplateFunc {
+	public static final IfChildrenFunc INSTANCE = new IfChildrenFunc();
+	
+	private static final Pattern CHILDREN_OF_PLACEHOLDER_PATTERN = Pattern.compile("\\s*[a-zA-Z_][a-zA-Z0-9_]*(\\.[a-zA-Z_][a-zA-Z0-9_]*)*\\s*,");
+	
+	private static final Pattern CHILD_PLACEHOLDER_PATTERN = Pattern.compile("\\s*[a-zA-Z_][a-zA-Z0-9_]*(\\.[a-zA-Z_][a-zA-Z0-9_]*)*");
+	
+	private static final Pattern OP_PATTERN = Pattern.compile("\\s*(<|>|<=|>=|==|!=|\\sENDS_WITH\\s|\\sSTARTS_WITH\\s)\\s*");
+	
+	private static final Pattern RVALUE_PATTERN = Pattern.compile("([a-zA-Z_][a-zA-Z0-9_]*(\\.[a-zA-Z_][a-zA-Z0-9_]*)*|\"(\\\\.|\\\\\\s|\\s|[^\\\\\"])*\")");
+	
+	private static final Pattern THEN_PATTERN = Pattern.compile("\\s*\\sTHEN\\s");
+	
+	private IfChildrenFunc() {
+		super("IFCHILDS", "ENDIFCHILDS", false);
+	}
+	
+	@Override
+	public Map<String, HTMLTemplatePlaceholder> handle(StringBuilder content, L2PcInstance player, Map<String, HTMLTemplatePlaceholder> placeholders, HTMLTemplateFunc[] funcs) {
+		try {
+			Matcher matcher = getMatcher(CHILDREN_OF_PLACEHOLDER_PATTERN, content, 0);
+			String childrenPlaceholderString = matcher.group().substring(0, matcher.group().length() - 1);
+			HTMLTemplatePlaceholder childrenPlaceholder = HTMLTemplateUtils.getPlaceholder(childrenPlaceholderString, placeholders);
+			if (childrenPlaceholder == null) {
+				content.setLength(0);
+				return null;
+			}
+			
+			matcher = getMatcher(CHILD_PLACEHOLDER_PATTERN, content, matcher.end());
+			String childPlaceholderString = matcher.group().trim();
+			int findIndex = matcher.end();
+			
+			matcher = getMatcher(OP_PATTERN, content, findIndex);
+			String op = matcher.group().trim();
+			findIndex = matcher.end();
+			
+			matcher = getMatcher(RVALUE_PATTERN, content, findIndex);
+			String rValue = matcher.group();
+			if (rValue.charAt(0) == '"') {
+				rValue = rValue.substring(1, rValue.length() - 1);
+			} else {
+				rValue = HTMLTemplateUtils.getPlaceholderValue(rValue, placeholders);
+			}
+			findIndex = matcher.end();
+			
+			matcher = getMatcher(THEN_PATTERN, content, findIndex);
+			findIndex = matcher.end();
+			
+			for (Entry<String, HTMLTemplatePlaceholder> entry : childrenPlaceholder.getChildren().entrySet()) {
+				HTMLTemplatePlaceholder childPlaceholder = entry.getValue().getChild(childPlaceholderString);
+				if (childPlaceholder == null) {
+					continue;
+				}
+				
+				try {
+					boolean ok = false;
+					switch (op) {
+						case "<":
+							ok = Integer.parseInt(childPlaceholder.getValue()) < Integer.parseInt(rValue);
+							break;
+						case ">":
+							ok = Integer.parseInt(childPlaceholder.getValue()) > Integer.parseInt(rValue);
+							break;
+						case "<=":
+							ok = Integer.parseInt(childPlaceholder.getValue()) <= Integer.parseInt(rValue);
+							break;
+						case ">=":
+							ok = Integer.parseInt(childPlaceholder.getValue()) >= Integer.parseInt(rValue);
+							break;
+						case "==":
+							ok = childPlaceholder.getValue().equals(rValue);
+							break;
+						case "!=":
+							ok = !childPlaceholder.getValue().equals(rValue);
+							break;
+						case "ENDS_WITH":
+							ok = childPlaceholder.getValue().endsWith(rValue);
+							break;
+						case "STARTS_WITH":
+							ok = childPlaceholder.getValue().startsWith(rValue);
+							break;
+					}
+					
+					if (!ok) {
+						// condition is not met, no content to show
+						content.setLength(0);
+						return null;
+					}
+				} catch (Exception e) {
+					// on an exception the types are incompatible with the operator, this function ignores such cases
+				}
+			}
+			
+			content.delete(0, findIndex);
+		} catch (Exception ex) {
+			content.setLength(0);
+		}
+		return null;
+	}
+	
+	private static Matcher getMatcher(Pattern pattern, StringBuilder content, int findIndex) throws Exception {
+		Matcher m = pattern.matcher(content);
+		if (!m.find(findIndex) || (m.start() > findIndex)) {
+			throw new Exception();
+		}
+		
+		return m;
+	}
+}
diff --git a/src/main/java/com/l2jserver/datapack/custom/service/base/util/htmltmpls/funcs/IfFunc.java b/src/main/java/com/l2jserver/datapack/custom/service/base/util/htmltmpls/funcs/IfFunc.java
new file mode 100644
index 0000000000..47eb87fb31
--- /dev/null
+++ b/src/main/java/com/l2jserver/datapack/custom/service/base/util/htmltmpls/funcs/IfFunc.java
@@ -0,0 +1,136 @@
+/*
+ * Copyright © 2004-2020 L2J DataPack
+ * 
+ * This file is part of L2J DataPack.
+ * 
+ * L2J DataPack 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 DataPack 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 <http://www.gnu.org/licenses/>.
+ */
+package com.l2jserver.datapack.custom.service.base.util.htmltmpls.funcs;
+
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import com.l2jserver.datapack.custom.service.base.util.htmltmpls.HTMLTemplateFunc;
+import com.l2jserver.datapack.custom.service.base.util.htmltmpls.HTMLTemplatePlaceholder;
+import com.l2jserver.datapack.custom.service.base.util.htmltmpls.HTMLTemplateUtils;
+import com.l2jserver.gameserver.model.actor.instance.L2PcInstance;
+
+/**
+ * If function.
+ * This class implements the following function syntax:<br>
+ * [IF(placeholder_name == "text in string" THEN text when the condition matches)ENDIF]<br>
+ * [IF(placeholder_name == another_placeholder_name THEN text when the condition matches)ENDIF]<br>
+ * [IF(int_placeholder_name > another_int_placeholder_name THEN text when the condition matches)ENDIF]<br>
+ * <br>
+ * First is always a placeholder name.<br>
+ * <br>
+ * Second comes one of the following operators:<br>
+ * <, >, <=, >=, !=, ==, STARTS_WITH or ENDS_WITH, where <, >, <= and >= are only to be used with placeholders/strings<br>
+ * which have a numeric value.<br>
+ * <br>
+ * Third comes either a placeholder name or a string("text in string").<br>
+ * <br>
+ * After the "THEN" word comes the text to place in the content when the condition is met.
+ * @author HorridoJoho
+ * @version 2.6.2.0
+ */
+public final class IfFunc extends HTMLTemplateFunc {
+	public static final IfFunc INSTANCE = new IfFunc();
+	
+	private static final Pattern LVALUE_PATTERN = Pattern.compile("\\s*[a-zA-Z_][a-zA-Z0-9_]*(\\.[a-zA-Z_][a-zA-Z0-9_]*)*");
+	
+	private static final Pattern OP_PATTERN = Pattern.compile("\\s*(<|>|<=|>=|==|!=|\\sENDS_WITH\\s|\\sSTARTS_WITH\\s)\\s*");
+	
+	private static final Pattern RVALUE_PATTERN = Pattern.compile("([a-zA-Z_][a-zA-Z0-9_]*(\\.[a-zA-Z_][a-zA-Z0-9_]*)*|\"(\\\\.|\\\\\\s|\\s|[^\\\\\"])*\")");
+	
+	private static final Pattern THEN_PATTERN = Pattern.compile("\\s*\\sTHEN\\s");
+	
+	private IfFunc() {
+		super("IF", "ENDIF", false);
+	}
+	
+	@Override
+	public Map<String, HTMLTemplatePlaceholder> handle(StringBuilder content, L2PcInstance player, Map<String, HTMLTemplatePlaceholder> placeholders, HTMLTemplateFunc[] funcs) {
+		try {
+			Matcher matcher = getMatcher(LVALUE_PATTERN, content, 0);
+			String lValue = HTMLTemplateUtils.getPlaceholderValue(matcher.group().trim(), placeholders);
+			int findIndex = matcher.end();
+			
+			matcher = getMatcher(OP_PATTERN, content, findIndex);
+			String op = matcher.group().trim();
+			findIndex = matcher.end();
+			
+			matcher = getMatcher(RVALUE_PATTERN, content, findIndex);
+			String rValue = matcher.group();
+			if (rValue.charAt(0) == '"') {
+				rValue = rValue.substring(1, rValue.length() - 1);
+			} else {
+				rValue = HTMLTemplateUtils.getPlaceholderValue(rValue, placeholders);
+			}
+			findIndex = matcher.end();
+			
+			matcher = getMatcher(THEN_PATTERN, content, findIndex);
+			findIndex = matcher.end();
+			
+			boolean ok = false;
+			switch (op) {
+				case "<":
+					ok = Integer.parseInt(lValue) < Integer.parseInt(rValue);
+					break;
+				case ">":
+					ok = Integer.parseInt(lValue) > Integer.parseInt(rValue);
+					break;
+				case "<=":
+					ok = Integer.parseInt(lValue) <= Integer.parseInt(rValue);
+					break;
+				case ">=":
+					ok = Integer.parseInt(lValue) >= Integer.parseInt(rValue);
+					break;
+				case "==":
+					ok = lValue.equals(rValue);
+					break;
+				case "!=":
+					ok = !lValue.equals(rValue);
+					break;
+				case "ENDS_WITH":
+					ok = lValue.endsWith(rValue);
+					break;
+				case "STARTS_WITH":
+					ok = lValue.startsWith(rValue);
+					break;
+			}
+			
+			if (ok) {
+				// this ensures only the replacement content is left
+				content.delete(0, findIndex);
+			} else {
+				// condition is not met, no content to show
+				content.setLength(0);
+				return null;
+			}
+		} catch (Exception ex) {
+			content.setLength(0);
+		}
+		return null;
+	}
+	
+	private static Matcher getMatcher(Pattern pattern, StringBuilder content, int findIndex) throws Exception {
+		Matcher m = pattern.matcher(content);
+		if (!m.find(findIndex) || (m.start() > findIndex)) {
+			throw new Exception();
+		}
+		return m;
+	}
+}
diff --git a/src/main/java/com/l2jserver/datapack/custom/service/base/util/htmltmpls/funcs/IncludeFunc.java b/src/main/java/com/l2jserver/datapack/custom/service/base/util/htmltmpls/funcs/IncludeFunc.java
new file mode 100644
index 0000000000..14a55d20a0
--- /dev/null
+++ b/src/main/java/com/l2jserver/datapack/custom/service/base/util/htmltmpls/funcs/IncludeFunc.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright © 2004-2020 L2J DataPack
+ * 
+ * This file is part of L2J DataPack.
+ * 
+ * L2J DataPack 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 DataPack 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 <http://www.gnu.org/licenses/>.
+ */
+package com.l2jserver.datapack.custom.service.base.util.htmltmpls.funcs;
+
+import java.util.Map;
+
+import com.l2jserver.datapack.custom.service.base.util.htmltmpls.HTMLTemplateFunc;
+import com.l2jserver.datapack.custom.service.base.util.htmltmpls.HTMLTemplatePlaceholder;
+import com.l2jserver.gameserver.cache.HtmCache;
+import com.l2jserver.gameserver.model.actor.instance.L2PcInstance;
+
+/**
+ * Include function.
+ * @author HorridoJoho
+ * @version 2.6.2.0
+ */
+public final class IncludeFunc extends HTMLTemplateFunc {
+	public static final IncludeFunc INSTANCE = new IncludeFunc();
+	
+	private IncludeFunc() {
+		super("INC", "ENDINC", true);
+	}
+	
+	@Override
+	public Map<String, HTMLTemplatePlaceholder> handle(StringBuilder content, L2PcInstance player, Map<String, HTMLTemplatePlaceholder> placeholders, HTMLTemplateFunc[] funcs) {
+		String fileContent = HtmCache.getInstance().getHtm(player != null ? player.getHtmlPrefix() : null, content.toString());
+		if (fileContent != null) {
+			content.ensureCapacity(fileContent.length());
+			content.setLength(0);
+			content.append(fileContent);
+		}
+		return null;
+	}
+}
diff --git a/src/main/java/com/l2jserver/datapack/custom/service/buffer/BufferService.java b/src/main/java/com/l2jserver/datapack/custom/service/buffer/BufferService.java
new file mode 100644
index 0000000000..14faec5607
--- /dev/null
+++ b/src/main/java/com/l2jserver/datapack/custom/service/buffer/BufferService.java
@@ -0,0 +1,539 @@
+/*
+ * Copyright © 2004-2020 L2J DataPack
+ * 
+ * This file is part of L2J DataPack.
+ * 
+ * L2J DataPack 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 DataPack 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 <http://www.gnu.org/licenses/>.
+ */
+package com.l2jserver.datapack.custom.service.buffer;
+
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.concurrent.ConcurrentHashMap;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.l2jserver.datapack.custom.service.base.CustomServiceScript;
+import com.l2jserver.datapack.custom.service.base.util.CommandProcessor;
+import com.l2jserver.datapack.custom.service.base.util.htmltmpls.HTMLTemplatePlaceholder;
+import com.l2jserver.datapack.custom.service.buffer.model.entity.AbstractBuffer;
+import com.l2jserver.datapack.custom.service.buffer.model.entity.BuffCategory;
+import com.l2jserver.datapack.custom.service.buffer.model.entity.BuffSkill;
+import com.l2jserver.gameserver.config.Configuration;
+import com.l2jserver.gameserver.handler.BypassHandler;
+import com.l2jserver.gameserver.handler.ItemHandler;
+import com.l2jserver.gameserver.handler.VoicedCommandHandler;
+import com.l2jserver.gameserver.model.actor.L2Npc;
+import com.l2jserver.gameserver.model.actor.L2Playable;
+import com.l2jserver.gameserver.model.actor.instance.L2PcInstance;
+import com.l2jserver.gameserver.model.skills.BuffInfo;
+
+/**
+ * Buffer Service.
+ * @author HorridoJoho
+ * @version 2.6.2.0
+ */
+public final class BufferService extends CustomServiceScript {
+	
+	private static final Logger LOG = LoggerFactory.getLogger(BufferService.class);
+	
+	public static final String SCRIPT_NAME = "buffer";
+	
+	public static final Path SCRIPT_PATH = Paths.get(SCRIPT_COLLECTION, SCRIPT_NAME);
+	
+	public static void main(String[] args) {
+		if (!Configuration.bufferService().enable()) {
+			LOG.info("Disabled.");
+			return;
+		}
+		
+		LOG.info("Loading...");
+		
+		BufferServiceRepository.getInstance().getConfig().registerNpcs(getInstance());
+	}
+	
+	private static final Map<Integer, Long> LAST_PLAYABLES_HEAL_TIME = new ConcurrentHashMap<>();
+	
+	private static final Map<Integer, String> ACTIVE_PLAYER_BUFFLISTS = new ConcurrentHashMap<>();
+	
+	BufferService() {
+		super(SCRIPT_NAME);
+		
+		BypassHandler.getInstance().registerHandler(BufferServiceBypassHandler.getInstance());
+		
+		if (Configuration.bufferService().getVoicedEnable()) {
+			VoicedCommandHandler.getInstance().registerHandler(BufferServiceVoicedCommandHandler.getInstance());
+			ItemHandler.getInstance().registerHandler(BufferServiceItemHandler.getInstance());
+		}
+	}
+	
+	@Override
+	public boolean unload() {
+		BypassHandler.getInstance().removeHandler(BufferServiceBypassHandler.getInstance());
+		if (Configuration.bufferService().getVoicedEnable()) {
+			VoicedCommandHandler.getInstance().removeHandler(BufferServiceVoicedCommandHandler.getInstance());
+			ItemHandler.getInstance().removeHandler(BufferServiceItemHandler.getInstance());
+		}
+		return super.unload();
+	}
+	
+	private void castBuff(L2Playable playable, BuffSkill buff) {
+		buff.getSkill().applyEffects(playable, playable);
+	}
+	
+	private void showAdvancedHtml(L2PcInstance player, AbstractBuffer buffer, L2Npc npc, String htmlPath, Map<String, HTMLTemplatePlaceholder> placeholders) {
+		HTMLTemplatePlaceholder ulistsPlaceholder = BufferServiceRepository.getInstance().getPlayersUListsPlaceholder(player.getObjectId());
+		if (ulistsPlaceholder != null) {
+			placeholders.put(ulistsPlaceholder.getName(), ulistsPlaceholder);
+		}
+		
+		String activeUniqueName = ACTIVE_PLAYER_BUFFLISTS.get(player.getObjectId());
+		if (activeUniqueName != null) {
+			HTMLTemplatePlaceholder ulistPlaceholder = BufferServiceRepository.getInstance().getPlayersUListPlaceholder(player.getObjectId(), activeUniqueName);
+			if (ulistPlaceholder != null) {
+				placeholders.put("active_unique", ulistPlaceholder);
+			}
+		}
+		
+		HTMLTemplatePlaceholder playerPlaceholder = new HTMLTemplatePlaceholder("player", null);
+		playerPlaceholder.addChild("name", player.getName());
+		playerPlaceholder.addChild("unique_max_buffs", String.valueOf(player.getStat().getMaxBuffCount()));
+		playerPlaceholder.addChild("unique_max_songs_dances", String.valueOf(Configuration.character().getMaxDanceAmount()));
+		
+		placeholders.put(playerPlaceholder.getName(), playerPlaceholder);
+		
+		super.showAdvancedHtml(player, buffer, npc, htmlPath, placeholders);
+	}
+	
+	private boolean htmlShowMain(L2PcInstance player, AbstractBuffer buffer, L2Npc npc) {
+		showAdvancedHtml(player, buffer, npc, "main.html", new HashMap<>());
+		return true;
+	}
+	
+	private boolean htmlShowCategory(L2PcInstance player, AbstractBuffer buffer, L2Npc npc, String categoryIdent) {
+		BuffCategory buffCat = buffer.getBuffCats().get(categoryIdent);
+		if (buffCat == null) {
+			debug(player, "Invalid buff category: " + categoryIdent);
+			return false;
+		}
+		
+		HashMap<String, HTMLTemplatePlaceholder> placeholders = new HashMap<>();
+		
+		placeholders.put("category", buffCat.getPlaceholder());
+		
+		showAdvancedHtml(player, buffer, npc, "category.html", placeholders);
+		return true;
+	}
+	
+	private boolean htmlShowBuff(L2PcInstance player, AbstractBuffer buffer, L2Npc npc, String categoryIdent, String buffIdent) {
+		BuffCategory buffCat = buffer.getBuffCats().get(categoryIdent);
+		if (buffCat == null) {
+			debug(player, "Invalid buff category: " + categoryIdent);
+			return false;
+		}
+		BuffSkill buff = buffCat.getBuff(buffIdent);
+		if (buff == null) {
+			debug(player, "Invalid buff skill: " + buffIdent);
+			return false;
+		}
+		
+		HashMap<String, HTMLTemplatePlaceholder> placeholders = new HashMap<>();
+		
+		placeholders.put("category", buffCat.getPlaceholder());
+		placeholders.put("buff", buff.getPlaceholder());
+		
+		showAdvancedHtml(player, buffer, npc, "buff.html", placeholders);
+		return true;
+	}
+	
+	private boolean htmlShowPreset(L2PcInstance player, AbstractBuffer buffer, L2Npc npc, String presetBufflistIdent) {
+		BuffCategory presetBufflist = buffer.getPresetBuffCats().get(presetBufflistIdent);
+		if (presetBufflist == null) {
+			debug(player, "Invalid preset buff category: " + presetBufflistIdent);
+			return false;
+		}
+		
+		var placeholders = new HashMap<String, HTMLTemplatePlaceholder>();
+		
+		placeholders.put("preset", presetBufflist.getPlaceholder());
+		
+		showAdvancedHtml(player, buffer, npc, "preset.html", placeholders);
+		return true;
+	}
+	
+	private boolean htmlShowUnique(L2PcInstance player, AbstractBuffer buffer, L2Npc npc, String uniqueName) {
+		HTMLTemplatePlaceholder uniquePlaceholder = BufferServiceRepository.getInstance().getPlayersUListPlaceholder(player.getObjectId(), uniqueName);
+		if (uniquePlaceholder == null) {
+			// redirect to main html if uniqueName is not valid, will most likely happen when the player deletes a unique bufflist he is currently viewing
+			executeHtmlCommand(player, npc, new CommandProcessor("main"));
+			return false;
+		}
+		
+		HashMap<String, HTMLTemplatePlaceholder> placeholders = new HashMap<>();
+		
+		placeholders.put(uniquePlaceholder.getName(), uniquePlaceholder);
+		
+		showAdvancedHtml(player, buffer, npc, "unique.html", placeholders);
+		return true;
+	}
+	
+	private void targetBuffBuff(L2PcInstance player, L2Playable target, AbstractBuffer buffer, String categoryIdent, String buffIdent) {
+		BuffCategory bCat = buffer.getBuffCats().get(categoryIdent);
+		if (bCat == null) {
+			debug(player, "Invalid buff category: " + categoryIdent);
+			return;
+		}
+		BuffSkill buff = bCat.getBuff(buffIdent);
+		if (buff == null) {
+			debug(player, "Invalid buff skill: " + buffIdent);
+			return;
+		}
+		
+		if (!buff.getItems().isEmpty()) {
+			HashMap<Integer, Long> items = new HashMap<>();
+			fillItemAmountMap(items, buff);
+			
+			for (Entry<Integer, Long> item : items.entrySet()) {
+				if (player.getInventory().getInventoryItemCount(item.getKey(), 0, true) < item.getValue()) {
+					player.sendMessage("Not enough items!");
+					return;
+				}
+			}
+			
+			for (Entry<Integer, Long> item : items.entrySet()) {
+				player.destroyItemByItemId("BufferService", item.getKey(), item.getValue(), player, true);
+			}
+		}
+		
+		castBuff(target, buff);
+	}
+	
+	private void targetBuffUnique(L2PcInstance player, L2Playable target, AbstractBuffer buffer, String uniqueName) {
+		List<BuffSkill> buffs = BufferServiceRepository.getInstance().getUniqueBufflist(player.getObjectId(), uniqueName);
+		
+		if (buffs != null) {
+			HashMap<Integer, Long> items = null;
+			for (BuffSkill buff : buffs) {
+				if (!buff.getItems().isEmpty()) {
+					if (items == null) {
+						items = new HashMap<>();
+					}
+					fillItemAmountMap(items, buff);
+				}
+			}
+			
+			if (items != null) {
+				for (var item : items.entrySet()) {
+					if (player.getInventory().getInventoryItemCount(item.getKey(), 0, true) < item.getValue()) {
+						player.sendMessage("Not enough items!");
+						return;
+					}
+				}
+				
+				for (var item : items.entrySet()) {
+					player.destroyItemByItemId("BufferService", item.getKey(), item.getValue(), player, true);
+				}
+			}
+			
+			for (BuffSkill buff : buffs) {
+				castBuff(target, buff);
+			}
+		}
+	}
+	
+	private void targetBuffPreset(L2PcInstance player, L2Playable target, AbstractBuffer buffer, String presetBufflistIdent) {
+		BuffCategory presetBufflist = buffer.getPresetBuffCats().get(presetBufflistIdent);
+		if (presetBufflist == null) {
+			debug(player, "Invalid preset buff category: " + presetBufflistIdent);
+			return;
+		}
+		
+		Collection<BuffSkill> buffs = presetBufflist.getBuffs().values();
+		
+		if (buffs != null) {
+			HashMap<Integer, Long> items = null;
+			for (BuffSkill buff : buffs) {
+				if (!buff.getItems().isEmpty()) {
+					if (items == null) {
+						items = new HashMap<>();
+					}
+					fillItemAmountMap(items, buff);
+				}
+			}
+			
+			if (items != null) {
+				for (Entry<Integer, Long> item : items.entrySet()) {
+					if (player.getInventory().getInventoryItemCount(item.getKey(), 0, true) < item.getValue()) {
+						player.sendMessage("Not enough items!");
+						return;
+					}
+				}
+				
+				for (Entry<Integer, Long> item : items.entrySet()) {
+					player.destroyItemByItemId("BufferService", item.getKey(), item.getValue(), player, true);
+				}
+			}
+			
+			for (BuffSkill buff : buffs) {
+				castBuff(target, buff);
+			}
+		}
+	}
+	
+	private void targetHeal(L2PcInstance player, L2Playable target, AbstractBuffer buffer) {
+		if (!buffer.getCanHeal()) {
+			debug(player, "This buffer can not heal!");
+			return;
+		}
+		
+		// prevent heal spamming, process cooldown on heal target
+		Long lastPlayableHealTime = LAST_PLAYABLES_HEAL_TIME.get(target.getObjectId());
+		if (lastPlayableHealTime != null) {
+			Long elapsedTime = System.currentTimeMillis() - lastPlayableHealTime;
+			Long healCooldown = Configuration.bufferService().getHealCooldown();
+			if (elapsedTime < healCooldown) {
+				long remainingTime = healCooldown - elapsedTime;
+				if (target == player) {
+					player.sendMessage("You can heal yourself again in " + (remainingTime / 1000) + " seconds.");
+				} else {
+					player.sendMessage("You can heal your pet again in " + (remainingTime / 1000) + " seconds.");
+				}
+				return;
+			}
+		}
+		
+		LAST_PLAYABLES_HEAL_TIME.put(target.getObjectId(), System.currentTimeMillis());
+		
+		if (player == target) {
+			player.setCurrentCp(player.getMaxCp());
+		}
+		target.setCurrentHp(target.getMaxHp());
+		target.setCurrentMp(target.getMaxMp());
+		target.broadcastStatusUpdate();
+	}
+	
+	private void targetCancel(L2PcInstance player, L2Playable target, AbstractBuffer buffer) {
+		if (!buffer.getCanCancel()) {
+			debug(player, "This buffer can not cancel!");
+			return;
+		}
+		target.stopAllEffectsExceptThoseThatLastThroughDeath();
+	}
+	
+	private void executeTargetCommand(L2PcInstance player, AbstractBuffer buffer, CommandProcessor command) {
+		// first determine the target
+		L2Playable target;
+		if (command.matchAndRemove("player ", "p ")) {
+			target = player;
+		} else if (command.matchAndRemove("summon ", "s ")) {
+			target = player.getSummon();
+			if (target == null) {
+				debug(player, "No summon available!");
+				return;
+			}
+		} else {
+			debug(player, "Invalid target command target!");
+			return;
+		}
+		
+		// run the chosen action on the target
+		if (command.matchAndRemove("buff ", "b ")) {
+			String[] argsSplit = command.splitRemaining(" ");
+			if (argsSplit.length != 2) {
+				debug(player, "Missing arguments!");
+				return;
+			}
+			targetBuffBuff(player, target, buffer, argsSplit[0], argsSplit[1]);
+		} else if (command.matchAndRemove("unique ", "u ")) {
+			targetBuffUnique(player, target, buffer, command.getRemaining());
+		} else if (command.matchAndRemove("preset ", "p ")) {
+			targetBuffPreset(player, target, buffer, command.getRemaining());
+		} else if (command.matchAndRemove("heal", "h")) {
+			targetHeal(player, target, buffer);
+		} else if (command.matchAndRemove("cancel", "c")) {
+			targetCancel(player, target, buffer);
+		}
+	}
+	
+	private boolean uniqueCreate(L2PcInstance player, String uniqueName) {
+		if (!BufferServiceRepository.getInstance().canHaveMoreBufflists(player)) {
+			player.sendMessage("Maximum number of unique bufflists reached!");
+			return false;
+		}
+		
+		// only allow alpha numeric names because we use this name on the htmls
+		if (!uniqueName.matches("[A-Za-z0-9]+")) {
+			return false;
+		}
+		
+		return BufferServiceRepository.getInstance().createUniqueBufflist(player.getObjectId(), uniqueName);
+	}
+	
+	private void uniqueDelete(L2PcInstance player, String uniqueName) {
+		BufferServiceRepository.getInstance().deleteUniqueBufflist(player.getObjectId(), uniqueName);
+		// also remove from active buff list when it's the deleted
+		String activeUniqueName = ACTIVE_PLAYER_BUFFLISTS.get(player.getObjectId());
+		if ((activeUniqueName != null) && activeUniqueName.equals(uniqueName)) {
+			ACTIVE_PLAYER_BUFFLISTS.remove(player.getObjectId());
+		}
+	}
+	
+	private void uniqueAdd(L2PcInstance player, AbstractBuffer buffer, String uniqueName, String categoryIdent, String buffIdent) {
+		BuffCategory bCat = buffer.getBuffCats().get(categoryIdent);
+		if (bCat == null) {
+			return;
+		}
+		BuffSkill buff = bCat.getBuff(buffIdent);
+		if (buff == null) {
+			return;
+		}
+		
+		BufferServiceRepository.getInstance().addToUniqueBufflist(player, uniqueName, buff);
+	}
+	
+	private void uniqueRemove(L2PcInstance player, String uniqueName, String buffIdent) {
+		BuffSkill buff = BufferServiceRepository.getInstance().getConfig().getGlobal().getBuff(buffIdent);
+		if (buff == null) {
+			return;
+		}
+		
+		BufferServiceRepository.getInstance().removeFromUniqueBufflist(player.getObjectId(), uniqueName, buff);
+	}
+	
+	private void uniqueSelect(L2PcInstance player, String uniqueName) {
+		if (BufferServiceRepository.getInstance().hasUniqueBufflist(player.getObjectId(), uniqueName)) {
+			ACTIVE_PLAYER_BUFFLISTS.put(player.getObjectId(), uniqueName);
+		}
+	}
+	
+	private void uniqueDeselect(L2PcInstance player) {
+		ACTIVE_PLAYER_BUFFLISTS.remove(player.getObjectId());
+	}
+	
+	private void executeUniqueCommand(L2PcInstance player, AbstractBuffer buffer, CommandProcessor command) {
+		if (command.matchAndRemove("create ", "c ")) {
+			uniqueCreate(player, command.getRemaining());
+		} else if (command.matchAndRemove("create_from_effects ", "cfe ")) {
+			String uniqueName = command.getRemaining();
+			if (!uniqueCreate(player, uniqueName)) {
+				return;
+			}
+			
+			final List<BuffInfo> effects = player.getEffectList().getEffects();
+			for (final BuffInfo effect : effects) {
+				for (Entry<String, BuffCategory> buffCatEntry : buffer.getBuffCats().entrySet()) {
+					boolean added = false;
+					
+					for (Entry<String, BuffSkill> buffEntry : buffCatEntry.getValue().getBuffs().entrySet()) {
+						final BuffSkill buff = buffEntry.getValue();
+						
+						if (buff.getSkill().getId() == effect.getSkill().getId()) {
+							uniqueAdd(player, buffer, uniqueName, buffCatEntry.getKey(), buff.getId());
+							added = true;
+							break;
+						}
+					}
+					
+					if (added) {
+						break;
+					}
+				}
+			}
+		} else if (command.matchAndRemove("delete ", "del ")) {
+			uniqueDelete(player, command.getRemaining());
+		} else if (command.matchAndRemove("add ", "a ")) {
+			String[] argsSplit = command.splitRemaining(" ");
+			if (argsSplit.length != 3) {
+				debug(player, "Missing arguments!");
+				return;
+			}
+			uniqueAdd(player, buffer, argsSplit[0], argsSplit[1], argsSplit[2]);
+		} else if (command.matchAndRemove("remove ", "r ")) {
+			String[] argsSplit = command.splitRemaining(" ");
+			if (argsSplit.length != 2) {
+				debug(player, "Missing arguments!");
+				return;
+			}
+			uniqueRemove(player, argsSplit[0], argsSplit[1]);
+		} else if (command.matchAndRemove("select ", "s ")) {
+			uniqueSelect(player, command.getRemaining());
+		} else if (command.matchAndRemove("deselect", "des")) {
+			uniqueDeselect(player);
+		}
+	}
+	
+	@Override
+	public boolean executeHtmlCommand(L2PcInstance player, L2Npc npc, CommandProcessor command) {
+		AbstractBuffer buffer = BufferServiceRepository.getInstance().getConfig().determineBuffer(npc, player);
+		if (buffer == null) {
+			player.sendMessage("No authorization!");
+			return false;
+		}
+		
+		if (command.matchAndRemove("main", "m")) {
+			return htmlShowMain(player, buffer, npc);
+		} else if (command.matchAndRemove("category ", "c ")) {
+			return htmlShowCategory(player, buffer, npc, command.getRemaining());
+		} else if (command.matchAndRemove("preset ", "p ")) {
+			return htmlShowPreset(player, buffer, npc, command.getRemaining());
+		} else if (command.matchAndRemove("buff ", "b ")) {
+			String[] argsSplit = command.splitRemaining(" ");
+			if (argsSplit.length != 2) {
+				debug(player, "Missing arguments!");
+				return false;
+			}
+			return htmlShowBuff(player, buffer, npc, argsSplit[0], argsSplit[1]);
+		} else if (command.matchAndRemove("unique ", "u ")) {
+			return htmlShowUnique(player, buffer, npc, command.getRemaining());
+		}
+		
+		return false;
+	}
+	
+	@Override
+	public boolean executeActionCommand(L2PcInstance player, L2Npc npc, CommandProcessor command) {
+		AbstractBuffer buffer = BufferServiceRepository.getInstance().getConfig().determineBuffer(npc, player);
+		if (buffer == null) {
+			player.sendMessage("No authorization!");
+			return false;
+		}
+		
+		if (command.matchAndRemove("target ", "t ")) {
+			executeTargetCommand(player, buffer, command);
+		} else if (command.matchAndRemove("unique ", "u ")) {
+			executeUniqueCommand(player, buffer, command);
+		}
+		
+		return true;
+	}
+	
+	@Override
+	protected boolean isDebugEnabled() {
+		return Configuration.bufferService().getDebug();
+	}
+	
+	public static BufferService getInstance() {
+		return SingletonHolder.INSTANCE;
+	}
+	
+	private static final class SingletonHolder {
+		protected static final BufferService INSTANCE = new BufferService();
+	}
+}
diff --git a/src/main/java/com/l2jserver/datapack/custom/service/buffer/BufferServiceBypassHandler.java b/src/main/java/com/l2jserver/datapack/custom/service/buffer/BufferServiceBypassHandler.java
new file mode 100644
index 0000000000..3532000e93
--- /dev/null
+++ b/src/main/java/com/l2jserver/datapack/custom/service/buffer/BufferServiceBypassHandler.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright © 2004-2020 L2J DataPack
+ * 
+ * This file is part of L2J DataPack.
+ * 
+ * L2J DataPack 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 DataPack 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 <http://www.gnu.org/licenses/>.
+ */
+package com.l2jserver.datapack.custom.service.buffer;
+
+import com.l2jserver.gameserver.handler.IBypassHandler;
+import com.l2jserver.gameserver.model.actor.L2Character;
+import com.l2jserver.gameserver.model.actor.L2Npc;
+import com.l2jserver.gameserver.model.actor.instance.L2PcInstance;
+
+/**
+ * Buffer Service Bypass handler.
+ * @author HorridoJoho
+ * @version 2.6.2.0
+ */
+public class BufferServiceBypassHandler implements IBypassHandler {
+	
+	public static final String BYPASS = "BufferService";
+	
+	private static final String[] BYPASS_LIST = new String[] {
+		BYPASS
+	};
+	
+	private BufferServiceBypassHandler() {
+		// Do nothing.
+	}
+	
+	@Override
+	public boolean useBypass(String command, L2PcInstance activeChar, L2Character target) {
+		if ((target == null) || !target.isNpc()) {
+			return false;
+		}
+		
+		BufferService.getInstance().executeCommand(activeChar, (L2Npc) target, command.substring(BYPASS.length()).trim());
+		return true;
+	}
+	
+	@Override
+	public String[] getBypassList() {
+		return BYPASS_LIST;
+	}
+	
+	public static BufferServiceBypassHandler getInstance() {
+		return SingletonHolder.INSTANCE;
+	}
+	
+	private static final class SingletonHolder {
+		protected static final BufferServiceBypassHandler INSTANCE = new BufferServiceBypassHandler();
+	}
+}
diff --git a/src/main/java/com/l2jserver/datapack/custom/service/buffer/BufferServiceItemHandler.java b/src/main/java/com/l2jserver/datapack/custom/service/buffer/BufferServiceItemHandler.java
new file mode 100644
index 0000000000..7f06674673
--- /dev/null
+++ b/src/main/java/com/l2jserver/datapack/custom/service/buffer/BufferServiceItemHandler.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright © 2004-2020 L2J DataPack
+ * 
+ * This file is part of L2J DataPack.
+ * 
+ * L2J DataPack 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 DataPack 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 <http://www.gnu.org/licenses/>.
+ */
+package com.l2jserver.datapack.custom.service.buffer;
+
+import com.l2jserver.gameserver.handler.IItemHandler;
+import com.l2jserver.gameserver.model.actor.L2Playable;
+import com.l2jserver.gameserver.model.actor.instance.L2PcInstance;
+import com.l2jserver.gameserver.model.items.instance.L2ItemInstance;
+
+/**
+ * Buffer Service item handler.
+ * @author HorridoJoho
+ * @version 2.6.2.0
+ */
+public final class BufferServiceItemHandler implements IItemHandler {
+	
+	private BufferServiceItemHandler() {
+		// Do nothing.
+	}
+	
+	@Override
+	public boolean useItem(L2Playable playable, L2ItemInstance item, boolean forceUse) {
+		if (!playable.isPlayer()) {
+			return false;
+		}
+		
+		BufferService.getInstance().executeCommand((L2PcInstance) playable, null, null);
+		return true;
+	}
+	
+	public static BufferServiceItemHandler getInstance() {
+		return SingletonHolder.INSTANCE;
+	}
+	
+	private static final class SingletonHolder {
+		protected static final BufferServiceItemHandler INSTANCE = new BufferServiceItemHandler();
+	}
+}
diff --git a/src/main/java/com/l2jserver/datapack/custom/service/buffer/BufferServiceRepository.java b/src/main/java/com/l2jserver/datapack/custom/service/buffer/BufferServiceRepository.java
new file mode 100644
index 0000000000..36b050939a
--- /dev/null
+++ b/src/main/java/com/l2jserver/datapack/custom/service/buffer/BufferServiceRepository.java
@@ -0,0 +1,260 @@
+/*
+ * Copyright © 2004-2020 L2J DataPack
+ * 
+ * This file is part of L2J DataPack.
+ * 
+ * L2J DataPack 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 DataPack 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 <http://www.gnu.org/licenses/>.
+ */
+package com.l2jserver.datapack.custom.service.buffer;
+
+import static java.sql.Statement.RETURN_GENERATED_KEYS;
+
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.concurrent.ConcurrentHashMap;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.l2jserver.commons.database.ConnectionFactory;
+import com.l2jserver.datapack.custom.service.base.util.htmltmpls.HTMLTemplatePlaceholder;
+import com.l2jserver.datapack.custom.service.buffer.model.BufferConfig;
+import com.l2jserver.datapack.custom.service.buffer.model.UniqueBufflist;
+import com.l2jserver.datapack.custom.service.buffer.model.entity.BuffSkill;
+import com.l2jserver.gameserver.config.Configuration;
+import com.l2jserver.gameserver.model.actor.instance.L2PcInstance;
+
+/**
+ * Buffer Service Data.
+ * @author HorridoJoho
+ * @version 2.6.2.0
+ */
+public final class BufferServiceRepository {
+	public enum BuffType {
+		BUFF,
+		SONG_DANCE
+	}
+	
+	private static final Logger LOG = LoggerFactory.getLogger(BufferServiceRepository.class);
+	
+	private static final String SELECT_UNIQUE_BUFF_LISTS = "SELECT ulist_id, ulist_char_id, ulist_name FROM custom_buffer_service_ulists ORDER BY ulist_char_id";
+	
+	private static final String SELECT_UNIQUE_BUFF_LIST = "SELECT ulist_buff_ident FROM custom_buffer_service_ulist_buffs WHERE ulist_id=?";
+	
+	private static final String INSERT_UNIQUE_BUFF_LIST = "INSERT INTO custom_buffer_service_ulists (ulist_char_id, ulist_name) VALUES(?, ?)";
+
+	private static final String DELETE_UNIQUE_BUFF_LIST = "DELETE FROM custom_buffer_service_ulists WHERE ulist_char_id=? AND ulist_id=?";
+
+	private static final String INSERT_BUFF_TO_UNIQUE_BUFF_LIST = "INSERT INTO custom_buffer_service_ulist_buffs VALUES(?, ?)";
+
+	private static final String DELETE_BUFF_FROM_BUFF_LIST = "DELETE FROM custom_buffer_service_ulist_buffs WHERE ulist_id=? AND ulist_buff_ident=?";
+	
+	private final BufferConfig config;
+	
+	protected final ConcurrentHashMap<Integer, Map<Integer, UniqueBufflist>> uniqueBufflists = new ConcurrentHashMap<>();
+	
+	private BufferServiceRepository() {
+		config = new BufferConfig();
+		
+		loadUniqueBufflists();
+	}
+	
+	private void loadUniqueBufflists() {
+		try (var con = ConnectionFactory.getInstance().getConnection()) {
+			try (var st = con.createStatement();
+				var rs = st.executeQuery(SELECT_UNIQUE_BUFF_LISTS)) {
+				while (rs.next()) {
+					int charId = rs.getInt("ulist_char_id");
+					int ulistId = rs.getInt("ulist_id");
+					String ulistName = rs.getString("ulist_name");
+					
+					Map<Integer, UniqueBufflist> ulists = getPlayersULists(charId);
+					ulists.put(ulistId, new UniqueBufflist(ulistId, ulistName));
+				}
+			}
+			
+			for (var ulists : uniqueBufflists.entrySet()) {
+				for (var ulist : ulists.getValue().entrySet()) {
+					try (var ps = con.prepareStatement(SELECT_UNIQUE_BUFF_LIST)) {
+						ps.setInt(1, ulist.getKey());
+						try (var rs = ps.executeQuery()) {
+							while (rs.next()) {
+								String buffIdent = rs.getString("ulist_buff_ident");
+								BuffSkill buff = config.getGlobal().getBuff(buffIdent);
+								if (buff == null) {
+									LOG.warn("BufferService - Data: Buff with ident does not exists!");
+								} else {
+									ulist.getValue().add(buff);
+								}
+							}
+						}
+					}
+				}
+			}
+		} catch (Exception ex) {
+			LOG.error("Error loading unique buffs!", ex);
+		}
+	}
+	
+	private Map<Integer, UniqueBufflist> getPlayersULists(int playerObjectId) {
+		return uniqueBufflists.computeIfAbsent(playerObjectId, k -> new LinkedHashMap<>());
+	}
+	
+	private UniqueBufflist getPlayersUList(int playerObjectId, String ulistName) {
+		Map<Integer, UniqueBufflist> ulists = getPlayersULists(playerObjectId);
+		for (Entry<Integer, UniqueBufflist> entry : ulists.entrySet()) {
+			if (entry.getValue().ulistName.equals(ulistName)) {
+				return entry.getValue();
+			}
+		}
+		return null;
+	}
+	
+	public boolean createUniqueBufflist(int playerObjectId, String ulistName) {
+		// prevent duplicate entry
+		if (getPlayersUList(playerObjectId, ulistName) != null) {
+			return false;
+		}
+		
+		try (var con = ConnectionFactory.getInstance().getConnection();
+			var ps = con.prepareStatement(INSERT_UNIQUE_BUFF_LIST, RETURN_GENERATED_KEYS)) {
+			ps.setInt(1, playerObjectId);
+			ps.setString(2, ulistName);
+			ps.executeUpdate();
+			try (var rs = ps.getGeneratedKeys()) {
+				if (rs.next()) {
+					int newId = rs.getInt(1);
+					getPlayersULists(playerObjectId).put(newId, new UniqueBufflist(newId, ulistName));
+				}
+				return true;
+			}
+		} catch (Exception ex) {
+			LOG.warn("Failed to insert unique bufflist!", ex);
+			return false;
+		}
+	}
+	
+	public void deleteUniqueBufflist(int playerObjectId, String ulistName) {
+		UniqueBufflist ulist = getPlayersUList(playerObjectId, ulistName);
+		if (ulist == null) {
+			return;
+		}
+		
+		try (var con = ConnectionFactory.getInstance().getConnection();
+			var ps = con.prepareStatement(DELETE_UNIQUE_BUFF_LIST)) {
+			ps.setInt(1, playerObjectId);
+			ps.setInt(2, ulist.ulistId);
+			ps.executeUpdate();
+			getPlayersULists(playerObjectId).remove(ulist.ulistId);
+		} catch (Exception ex) {
+			LOG.warn("Failed to delete unique bufflist!", ex);
+		}
+	}
+	
+	public boolean addToUniqueBufflist(L2PcInstance player, String ulistName, BuffSkill buff) {
+		UniqueBufflist ulist = getPlayersUList(player.getObjectId(), ulistName);
+		// prevent duplicate entry with ulist.contains(buff)
+		if ((ulist == null) || ulist.contains(buff) || ((buff.getType() == BuffType.BUFF) && (ulist.numBuffs >= player.getStat().getMaxBuffCount())) || ((buff.getType() == BuffType.SONG_DANCE) && (ulist.numSongsDances >= Configuration.character().getMaxDanceAmount()))) {
+			return false;
+		}
+		
+		if (addToUniqueBufflist(player.getObjectId(), ulist.ulistId, buff.getId())) {
+			ulist.add(buff);
+			return true;
+		}
+		return false;
+	}
+	
+	private boolean addToUniqueBufflist(int playerObjectId, int ulistId, String buffId) {
+		try (var con = ConnectionFactory.getInstance().getConnection();
+			var ps = con.prepareStatement(INSERT_BUFF_TO_UNIQUE_BUFF_LIST)) {
+			ps.setInt(1, ulistId);
+			ps.setString(2, buffId);
+			ps.executeUpdate();
+		} catch (Exception ex) {
+			LOG.warn("Failed to insert buff into unique bufflist!", ex);
+			return false;
+		}
+		return true;
+	}
+	
+	public void removeFromUniqueBufflist(int playerObjectId, String ulistName, BuffSkill buff) {
+		UniqueBufflist ulist = getPlayersUList(playerObjectId, ulistName);
+		if ((ulist == null) || !ulist.contains(buff)) {
+			return;
+		}
+		
+		try (var con = ConnectionFactory.getInstance().getConnection();
+			var ps = con.prepareStatement(DELETE_BUFF_FROM_BUFF_LIST)) {
+			ps.setInt(1, ulist.ulistId);
+			ps.setString(2, buff.getId());
+			ps.executeUpdate();
+			ulist.remove(buff);
+		} catch (Exception ex) {
+			LOG.warn("Failed to remove buff from unique bufflist!", ex);
+		}
+	}
+	
+	public BufferConfig getConfig() {
+		return config;
+	}
+	
+	public boolean canHaveMoreBufflists(L2PcInstance player) {
+		return getPlayersULists(player.getObjectId()).size() < Configuration.bufferService().getMaxUniqueLists();
+	}
+	
+	public boolean hasUniqueBufflist(int playerObjectId, String ulistName) {
+		return getPlayersUList(playerObjectId, ulistName) != null;
+	}
+	
+	public List<BuffSkill> getUniqueBufflist(int playerObjectId, String ulistName) {
+		UniqueBufflist ulist = getPlayersUList(playerObjectId, ulistName);
+		if (ulist == null) {
+			return null;
+		}
+		return Collections.unmodifiableList(ulist);
+	}
+	
+	public HTMLTemplatePlaceholder getPlayersUListPlaceholder(int playerObjectId, String ulistName) {
+		UniqueBufflist ulist = getPlayersUList(playerObjectId, ulistName);
+		if (ulist == null) {
+			return null;
+		}
+		return ulist.placeholder;
+	}
+	
+	public HTMLTemplatePlaceholder getPlayersUListsPlaceholder(int playerObjectId) {
+		Map<Integer, UniqueBufflist> ulists = getPlayersULists(playerObjectId);
+		if (ulists.isEmpty()) {
+			return null;
+		}
+		
+		HTMLTemplatePlaceholder placeholder = new HTMLTemplatePlaceholder("uniques", null);
+		for (Entry<Integer, UniqueBufflist> entry : ulists.entrySet()) {
+			placeholder.addAliasChild(String.valueOf(placeholder.getChildrenSize()), entry.getValue().placeholder);
+		}
+		return placeholder;
+	}
+	
+	public static BufferServiceRepository getInstance() {
+		return SingletonHolder.INSTANCE;
+	}
+	
+	private static class SingletonHolder {
+		static final BufferServiceRepository INSTANCE = new BufferServiceRepository();
+	}
+}
diff --git a/src/main/java/com/l2jserver/datapack/custom/service/buffer/BufferServiceVoicedCommandHandler.java b/src/main/java/com/l2jserver/datapack/custom/service/buffer/BufferServiceVoicedCommandHandler.java
new file mode 100644
index 0000000000..714f06655c
--- /dev/null
+++ b/src/main/java/com/l2jserver/datapack/custom/service/buffer/BufferServiceVoicedCommandHandler.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright © 2004-2020 L2J DataPack
+ * 
+ * This file is part of L2J DataPack.
+ * 
+ * L2J DataPack 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 DataPack 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 <http://www.gnu.org/licenses/>.
+ */
+package com.l2jserver.datapack.custom.service.buffer;
+
+import com.l2jserver.gameserver.config.Configuration;
+import com.l2jserver.gameserver.handler.IVoicedCommandHandler;
+import com.l2jserver.gameserver.model.actor.instance.L2PcInstance;
+
+/**
+ * Buffer service voiced command handler.
+ * @author HorridoJoho
+ * @version 2.6.2.0
+ */
+public final class BufferServiceVoicedCommandHandler implements IVoicedCommandHandler {
+	
+	private static final String[] COMMANDS = new String[] {
+		Configuration.bufferService().getVoicedCommand()
+	};
+	
+	private BufferServiceVoicedCommandHandler() {
+		// Do nothing.
+	}
+	
+	@Override
+	public boolean useVoicedCommand(String command, L2PcInstance activeChar, String params) {
+		BufferService.getInstance().executeCommand(activeChar, null, params);
+		return true;
+	}
+	
+	@Override
+	public String[] getVoicedCommandList() {
+		return COMMANDS;
+	}
+	
+	public static BufferServiceVoicedCommandHandler getInstance() {
+		return SingletonHolder.INSTANCE;
+	}
+	
+	private static final class SingletonHolder {
+		protected static final BufferServiceVoicedCommandHandler INSTANCE = new BufferServiceVoicedCommandHandler();
+	}
+}
diff --git a/src/main/java/com/l2jserver/datapack/custom/service/buffer/model/BufferConfig.java b/src/main/java/com/l2jserver/datapack/custom/service/buffer/model/BufferConfig.java
new file mode 100644
index 0000000000..ddd87f1b28
--- /dev/null
+++ b/src/main/java/com/l2jserver/datapack/custom/service/buffer/model/BufferConfig.java
@@ -0,0 +1,116 @@
+/*
+ * Copyright © 2004-2020 L2J DataPack
+ * 
+ * This file is part of L2J DataPack.
+ * 
+ * L2J DataPack 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 DataPack 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 <http://www.gnu.org/licenses/>.
+ */
+package com.l2jserver.datapack.custom.service.buffer.model;
+
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.gson.Gson;
+import com.l2jserver.datapack.custom.service.buffer.BufferService;
+import com.l2jserver.datapack.custom.service.buffer.model.entity.AbstractBuffer;
+import com.l2jserver.datapack.custom.service.buffer.model.entity.NpcBuffer;
+import com.l2jserver.datapack.custom.service.buffer.model.entity.VoicedBuffer;
+import com.l2jserver.gameserver.config.Configuration;
+import com.l2jserver.gameserver.model.actor.L2Npc;
+import com.l2jserver.gameserver.model.actor.instance.L2PcInstance;
+
+/**
+ * Buffer configuration.
+ * @author HorridoJoho
+ * @version 2.6.2.0
+ */
+public final class BufferConfig {
+	
+	private static final Logger LOG = LoggerFactory.getLogger(BufferConfig.class);
+	
+	private GlobalConfig global;
+	
+	private VoicedBuffer voiced;
+	
+	private Map<Integer, NpcBuffer> npcs;
+	
+	public BufferConfig() {
+		try {
+			final var gson = new Gson();
+			
+			final var jsonPath = Paths.get(Configuration.server().getDatapackRoot().getAbsolutePath(), "data", BufferService.SCRIPT_PATH.toString(), "json");
+			
+			global = gson.fromJson(Files.newBufferedReader(jsonPath.resolve("global.json")), GlobalConfig.class);
+			voiced = gson.fromJson(Files.newBufferedReader(jsonPath.resolve("voiced.json")), VoicedBuffer.class);
+			npcs = new HashMap<>();
+			
+			final var npcsDir = Paths.get(jsonPath.toString(), "npcs");
+			try (var dirStream = Files.newDirectoryStream(npcsDir, "*.json")) {
+				for (var entry : dirStream) {
+					if (!Files.isRegularFile(entry)) {
+						continue;
+					}
+					
+					final var npc = gson.fromJson(Files.newBufferedReader(entry), NpcBuffer.class);
+					npcs.put(npc.getId(), npc);
+				}
+			}
+			
+			global.afterDeserialize(this);
+			voiced.afterDeserialize(this);
+			for (var npc : npcs.values()) {
+				npc.afterDeserialize(this);
+			}
+		} catch (Exception ex) {
+			LOG.error("Error loading buffer configuration!", ex);
+		}
+	}
+	
+	public AbstractBuffer determineBuffer(L2Npc npc, L2PcInstance player) {
+		if (npc == null) {
+			if (!Configuration.bufferService().getVoicedEnable() || ((Configuration.bufferService().getVoicedRequiredItem() > 0) && (player.getInventory().getAllItemsByItemId(Configuration.bufferService().getVoicedRequiredItem()) == null))) {
+				return null;
+			}
+			return voiced;
+		}
+		return npcs.get(npc.getId());
+	}
+	
+	public void registerNpcs(BufferService scriptInstance) {
+		for (var npc : npcs.values()) {
+			if (npc.getDirectFirstTalk()) {
+				scriptInstance.addFirstTalkId(npc.getId());
+			}
+			scriptInstance.addStartNpc(npc.getId());
+			scriptInstance.addTalkId(npc.getId());
+		}
+	}
+	
+	public GlobalConfig getGlobal() {
+		return global;
+	}
+	
+	public final VoicedBuffer getVoiced() {
+		return voiced;
+	}
+	
+	public final Map<Integer, NpcBuffer> getNpcs() {
+		return npcs;
+	}
+}
\ No newline at end of file
diff --git a/src/main/java/com/l2jserver/datapack/custom/service/buffer/model/GlobalConfig.java b/src/main/java/com/l2jserver/datapack/custom/service/buffer/model/GlobalConfig.java
new file mode 100644
index 0000000000..9a9b6898c8
--- /dev/null
+++ b/src/main/java/com/l2jserver/datapack/custom/service/buffer/model/GlobalConfig.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright © 2004-2020 L2J DataPack
+ * 
+ * This file is part of L2J DataPack.
+ * 
+ * L2J DataPack 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 DataPack 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 <http://www.gnu.org/licenses/>.
+ */
+package com.l2jserver.datapack.custom.service.buffer.model;
+
+import java.util.Map;
+
+import com.l2jserver.datapack.custom.service.buffer.model.entity.BuffCategory;
+import com.l2jserver.datapack.custom.service.buffer.model.entity.BuffSkill;
+
+/**
+ * Global configuration.
+ * @author HorridoJoho
+ * @version 2.6.2.0
+ */
+public final class GlobalConfig {
+	private Map<String, BuffSkill> buffs;
+	private Map<String, BuffCategory> buffCategories;
+	
+	public void afterDeserialize(BufferConfig config) {
+		for (var buff : buffs.values()) {
+			buff.afterDeserialize(config);
+		}
+		
+		for (var category : buffCategories.values()) {
+			category.afterDeserialize(config);
+		}
+	}
+	
+	public BuffSkill getBuff(String id) {
+		return buffs.get(id);
+	}
+	
+	public final Map<String, BuffSkill> getBuffs() {
+		return buffs;
+	}
+	
+	public final Map<String, BuffCategory> getCategories() {
+		return buffCategories;
+	}
+}
\ No newline at end of file
diff --git a/src/main/java/com/l2jserver/datapack/custom/service/buffer/model/UniqueBufflist.java b/src/main/java/com/l2jserver/datapack/custom/service/buffer/model/UniqueBufflist.java
new file mode 100644
index 0000000000..f5cb17cc8a
--- /dev/null
+++ b/src/main/java/com/l2jserver/datapack/custom/service/buffer/model/UniqueBufflist.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright © 2004-2020 L2J DataPack
+ * 
+ * This file is part of L2J DataPack.
+ * 
+ * L2J DataPack 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 DataPack 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 <http://www.gnu.org/licenses/>.
+ */
+package com.l2jserver.datapack.custom.service.buffer.model;
+
+import java.util.LinkedList;
+
+import com.l2jserver.datapack.custom.service.base.util.htmltmpls.HTMLTemplatePlaceholder;
+import com.l2jserver.datapack.custom.service.buffer.BufferServiceRepository.BuffType;
+import com.l2jserver.datapack.custom.service.buffer.model.entity.BuffSkill;
+
+/**
+ * This class is here so we can actually get the name of this list and make placeholder adjustments easily while keeping outside code cleaner
+ * @author HorridoJoho
+ * @version 2.6.2.0
+ */
+public class UniqueBufflist extends LinkedList<BuffSkill> {
+	private static final long serialVersionUID = -2586607798277226501L;
+	
+	public final int ulistId;
+	public final String ulistName;
+	public int numBuffs;
+	public int numSongsDances;
+	public HTMLTemplatePlaceholder placeholder;
+	
+	public UniqueBufflist(int ulistId, String ulistName) {
+		this.ulistId = ulistId;
+		this.ulistName = ulistName;
+		this.numBuffs = 0;
+		this.numSongsDances = 0;
+		this.placeholder = new HTMLTemplatePlaceholder("unique", null).addChild("buffs", null).addChild("name", ulistName).addChild("num_buffs", "0").addChild("num_songs_dances", "0");
+	}
+	
+	@Override
+	public boolean add(BuffSkill e) {
+		if (super.add(e)) {
+			if (e.getType() == BuffType.BUFF) {
+				++this.numBuffs;
+				this.placeholder.getChild("num_buffs").setValue(String.valueOf(Integer.parseInt(this.placeholder.getChild("num_buffs").getValue()) + 1));
+			} else {
+				++this.numSongsDances;
+				this.placeholder.getChild("num_songs_dances").setValue(String.valueOf(Integer.parseInt(this.placeholder.getChild("num_songs_dances").getValue()) + 1));
+			}
+			this.placeholder.getChild("buffs").addAliasChild(e.getId(), e.getPlaceholder());
+			return true;
+		}
+		
+		return false;
+	}
+	
+	@Override
+	public boolean remove(Object o) {
+		if (super.remove(o)) {
+			switch (((BuffSkill) o).getType()) {
+				case BUFF:
+					--numBuffs;
+					break;
+				case SONG_DANCE:
+					--numSongsDances;
+					break;
+			}
+			
+			this.placeholder = new HTMLTemplatePlaceholder("unique", null).addChild("buffs", null).addChild("name", this.ulistName).addChild("num_buffs", String.valueOf(numBuffs)).addChild("num_songs_dances", String.valueOf(numSongsDances));
+			for (BuffSkill buff : this) {
+				this.placeholder.getChild("buffs").addAliasChild(buff.getId(), buff.getPlaceholder());
+			}
+			return true;
+		}
+		
+		return false;
+	}
+}
diff --git a/src/main/java/com/l2jserver/datapack/custom/service/buffer/model/entity/AbstractBuffer.java b/src/main/java/com/l2jserver/datapack/custom/service/buffer/model/entity/AbstractBuffer.java
new file mode 100644
index 0000000000..2aa0461dc6
--- /dev/null
+++ b/src/main/java/com/l2jserver/datapack/custom/service/buffer/model/entity/AbstractBuffer.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright © 2004-2020 L2J DataPack
+ * 
+ * This file is part of L2J DataPack.
+ * 
+ * L2J DataPack 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 DataPack 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 <http://www.gnu.org/licenses/>.
+ */
+package com.l2jserver.datapack.custom.service.buffer.model.entity;
+
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+
+import com.l2jserver.datapack.custom.service.base.model.entity.CustomServiceServer;
+import com.l2jserver.datapack.custom.service.base.util.htmltmpls.HTMLTemplatePlaceholder;
+import com.l2jserver.datapack.custom.service.buffer.model.BufferConfig;
+import com.l2jserver.gameserver.config.Configuration;
+
+/**
+ * Abstract buffer.
+ * @author HorridoJoho
+ * @version 2.6.2.0
+ */
+public abstract class AbstractBuffer extends CustomServiceServer {
+	private boolean canHeal;
+	private boolean canCancel;
+	private List<String> presetBuffCategories;
+	private List<String> buffCategories;
+	
+	private final transient Map<String, BuffCategory> presetBuffCatsMap = new LinkedHashMap<>();
+	private final transient Map<String, BuffCategory> buffCatsMap = new LinkedHashMap<>();
+	
+	public AbstractBuffer(String bypassPrefix) {
+		super(bypassPrefix, "buffer");
+	}
+	
+	public void afterDeserialize(BufferConfig config) {
+		super.afterDeserialize();
+		
+		for (String id : presetBuffCategories) {
+			presetBuffCatsMap.put(id, config.getGlobal().getCategories().get(id));
+		}
+		
+		for (String id : buffCategories) {
+			buffCatsMap.put(id, config.getGlobal().getCategories().get(id));
+		}
+		
+		if (canHeal) {
+			getPlaceholder().addChild("can_heal", null);
+		}
+		if (canCancel) {
+			getPlaceholder().addChild("can_cancel", null);
+		}
+		if (!presetBuffCategories.isEmpty()) {
+			HTMLTemplatePlaceholder presetBufflistsPlaceholder = getPlaceholder().addChild("presets", null).getChild("presets");
+			for (Entry<String, BuffCategory> presetBufflist : presetBuffCatsMap.entrySet()) {
+				presetBufflistsPlaceholder.addAliasChild(String.valueOf(presetBufflistsPlaceholder.getChildrenSize()), presetBufflist.getValue().getPlaceholder());
+			}
+		}
+		if (!buffCategories.isEmpty()) {
+			HTMLTemplatePlaceholder buffCatsPlaceholder = getPlaceholder().addChild("categories", null).getChild("categories");
+			for (Entry<String, BuffCategory> buffCat : buffCatsMap.entrySet()) {
+				buffCatsPlaceholder.addAliasChild(String.valueOf(buffCatsPlaceholder.getChildrenSize()), buffCat.getValue().getPlaceholder());
+			}
+		}
+		
+		getPlaceholder().addChild("max_unique_lists", String.valueOf(Configuration.bufferService().getMaxUniqueLists()));
+	}
+	
+	public final boolean getCanHeal() {
+		return canHeal;
+	}
+	
+	public final boolean getCanCancel() {
+		return canCancel;
+	}
+	
+	public Map<String, BuffCategory> getPresetBuffCats() {
+		return presetBuffCatsMap;
+	}
+	
+	public final Map<String, BuffCategory> getBuffCats() {
+		return buffCatsMap;
+	}
+}
diff --git a/src/main/java/com/l2jserver/datapack/custom/service/buffer/model/entity/BuffCategory.java b/src/main/java/com/l2jserver/datapack/custom/service/buffer/model/entity/BuffCategory.java
new file mode 100644
index 0000000000..935658b5e1
--- /dev/null
+++ b/src/main/java/com/l2jserver/datapack/custom/service/buffer/model/entity/BuffCategory.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright © 2004-2020 L2J DataPack
+ * 
+ * This file is part of L2J DataPack.
+ * 
+ * L2J DataPack 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 DataPack 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 <http://www.gnu.org/licenses/>.
+ */
+package com.l2jserver.datapack.custom.service.buffer.model.entity;
+
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+
+import com.l2jserver.datapack.custom.service.base.model.entity.Refable;
+import com.l2jserver.datapack.custom.service.base.util.htmltmpls.HTMLTemplatePlaceholder;
+import com.l2jserver.datapack.custom.service.buffer.model.BufferConfig;
+
+/**
+ * Buff category.
+ * @author HorridoJoho
+ * @version 2.6.2.0
+ */
+public class BuffCategory extends Refable {
+	private String name;
+	private List<String> buffs;
+	
+	private final transient Map<String, BuffSkill> buffSkillsMap = new LinkedHashMap<>();
+	
+	public void afterDeserialize(BufferConfig config) {
+		super.afterDeserialize();
+		
+		for (String id : buffs) {
+			buffSkillsMap.put(id, config.getGlobal().getBuff(id));
+		}
+		
+		getPlaceholder().addChild("name", name);
+		if (!buffs.isEmpty()) {
+			HTMLTemplatePlaceholder buffsPlaceholder = getPlaceholder().addChild("buffs", null).getChild("buffs");
+			for (Entry<String, BuffSkill> buff : buffSkillsMap.entrySet()) {
+				buffsPlaceholder.addAliasChild(String.valueOf(buffsPlaceholder.getChildrenSize()), buff.getValue().getPlaceholder());
+			}
+		}
+	}
+	
+	public String getName() {
+		return name;
+	}
+	
+	public Map<String, BuffSkill> getBuffs() {
+		return buffSkillsMap;
+	}
+	
+	public BuffSkill getBuff(String id) {
+		return buffSkillsMap.get(id);
+	}
+}
diff --git a/src/main/java/com/l2jserver/datapack/custom/service/buffer/model/entity/BuffSkill.java b/src/main/java/com/l2jserver/datapack/custom/service/buffer/model/entity/BuffSkill.java
new file mode 100644
index 0000000000..c26c60317c
--- /dev/null
+++ b/src/main/java/com/l2jserver/datapack/custom/service/buffer/model/entity/BuffSkill.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright © 2004-2020 L2J DataPack
+ * 
+ * This file is part of L2J DataPack.
+ * 
+ * L2J DataPack 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 DataPack 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 <http://www.gnu.org/licenses/>.
+ */
+package com.l2jserver.datapack.custom.service.buffer.model.entity;
+
+import com.l2jserver.datapack.custom.service.base.model.entity.CustomServiceProduct;
+import com.l2jserver.datapack.custom.service.buffer.BufferServiceRepository.BuffType;
+import com.l2jserver.datapack.custom.service.buffer.model.BufferConfig;
+import com.l2jserver.gameserver.datatables.SkillData;
+import com.l2jserver.gameserver.model.skills.Skill;
+
+/**
+ * Buff skill.
+ * @author HorridoJoho
+ * @version 2.6.2.0
+ */
+public class BuffSkill extends CustomServiceProduct {
+	private int skill;
+	private int level;
+	private BuffType type;
+	
+	public void afterDeserialize(BufferConfig config) {
+		super.afterDeserialize();
+		
+		final Skill skill = getSkill();
+		getPlaceholder().addChild("skill_id", String.valueOf(skill.getId())).addChild("skill_name", skill.getName()).addChild("skill_icon", skill.getIcon()).addChild("type", type.toString());
+	}
+	
+	public int getSkillId() {
+		return skill;
+	}
+	
+	public int getSkillLevel() {
+		return level;
+	}
+	
+	public BuffType getType() {
+		return type;
+	}
+	
+	public Skill getSkill() {
+		return SkillData.getInstance().getSkill(skill, level);
+	}
+	
+	@SuppressWarnings("unused")
+	private String getClientSkillIconSource(int skillId) {
+		String format;
+		if (skillId < 100) {
+			format = "00" + skillId;
+		} else if (skillId < 1000) {
+			format = "0" + skillId;
+		} else if (skillId == 1517) {
+			format = "1499";
+		} else if (skillId == 1518) {
+			format = "1502";
+		} else {
+			if ((skillId > 4698) && (skillId < 4701)) {
+				format = "1331";
+			} else if ((skillId > 4701) && (skillId < 4704)) {
+				format = "1332";
+			} else {
+				format = Integer.toString(skillId);
+			}
+		}
+		
+		return "icon.skill" + format;
+	}
+}
diff --git a/src/main/java/com/l2jserver/datapack/custom/service/buffer/model/entity/NpcBuffer.java b/src/main/java/com/l2jserver/datapack/custom/service/buffer/model/entity/NpcBuffer.java
new file mode 100644
index 0000000000..60c5687720
--- /dev/null
+++ b/src/main/java/com/l2jserver/datapack/custom/service/buffer/model/entity/NpcBuffer.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright © 2004-2020 L2J DataPack
+ * 
+ * This file is part of L2J DataPack.
+ * 
+ * L2J DataPack 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 DataPack 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 <http://www.gnu.org/licenses/>.
+ */
+package com.l2jserver.datapack.custom.service.buffer.model.entity;
+
+import com.l2jserver.datapack.custom.service.base.model.entity.IRefable;
+import com.l2jserver.datapack.custom.service.buffer.BufferServiceBypassHandler;
+import com.l2jserver.datapack.custom.service.buffer.model.BufferConfig;
+import com.l2jserver.gameserver.data.xml.impl.NpcData;
+import com.l2jserver.gameserver.model.actor.templates.L2NpcTemplate;
+
+/**
+ * NPC buffer.
+ * @author HorridoJoho
+ * @version 2.6.2.0
+ */
+public final class NpcBuffer extends AbstractBuffer implements IRefable<Integer> {
+	private Integer npcId;
+	private boolean directFirstTalk;
+	
+	public NpcBuffer() {
+		super(BufferServiceBypassHandler.BYPASS);
+	}
+	
+	@Override
+	public void afterDeserialize(BufferConfig config) {
+		super.afterDeserialize(config);
+		
+		getPlaceholder().addChild("ident", String.valueOf(npcId));
+	}
+	
+	public L2NpcTemplate getNpc() {
+		return NpcData.getInstance().getTemplate(npcId);
+	}
+	
+	@Override
+	public String getName() {
+		return getNpc().getName();
+	}
+	
+	@Override
+	public Integer getId() {
+		return npcId;
+	}
+	
+	public boolean getDirectFirstTalk() {
+		return directFirstTalk;
+	}
+}
diff --git a/src/main/java/com/l2jserver/datapack/custom/service/buffer/model/entity/VoicedBuffer.java b/src/main/java/com/l2jserver/datapack/custom/service/buffer/model/entity/VoicedBuffer.java
new file mode 100644
index 0000000000..f3e79cb9ef
--- /dev/null
+++ b/src/main/java/com/l2jserver/datapack/custom/service/buffer/model/entity/VoicedBuffer.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright © 2004-2020 L2J DataPack
+ * 
+ * This file is part of L2J DataPack.
+ * 
+ * L2J DataPack 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 DataPack 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 <http://www.gnu.org/licenses/>.
+ */
+package com.l2jserver.datapack.custom.service.buffer.model.entity;
+
+import com.l2jserver.datapack.custom.service.buffer.model.BufferConfig;
+import com.l2jserver.gameserver.config.Configuration;
+import com.l2jserver.gameserver.datatables.ItemTable;
+import com.l2jserver.gameserver.model.items.L2Item;
+
+/**
+ * Voiced buffer.
+ * @author HorridoJoho
+ * @version 2.6.2.0
+ */
+public final class VoicedBuffer extends AbstractBuffer {
+	public VoicedBuffer() {
+		super("voice ." + Configuration.bufferService().getVoicedCommand());
+	}
+	
+	@Override
+	public void afterDeserialize(BufferConfig config) {
+		super.afterDeserialize(config);
+	}
+	
+	public L2Item getRequiredItem() {
+		return ItemTable.getInstance().getTemplate(Configuration.bufferService().getVoicedRequiredItem());
+	}
+	
+	@Override
+	public String getName() {
+		return Configuration.bufferService().getVoicedName();
+	}
+}
diff --git a/src/main/java/com/l2jserver/datapack/quests/Q00325_GrimCollector/Q00325_GrimCollector.java b/src/main/java/com/l2jserver/datapack/quests/Q00325_GrimCollector/Q00325_GrimCollector.java
index afc65d3b41..7a6fdbc8c2 100644
--- a/src/main/java/com/l2jserver/datapack/quests/Q00325_GrimCollector/Q00325_GrimCollector.java
+++ b/src/main/java/com/l2jserver/datapack/quests/Q00325_GrimCollector/Q00325_GrimCollector.java
@@ -129,20 +129,20 @@ public final class Q00325_GrimCollector extends Quest {
 				final long totalCount = (head + heart + liver + skull + rib + spine + arm + thigh + complete);
 				if (totalCount > 0) {
 					long sum = ((head * 30) + (heart * 20) + (liver * 20) + (skull * 100) + (rib * 40) + (spine * 14) + (arm * 14) + (thigh * 14));
-						
+					
 					if (totalCount >= 10) {
 						sum += 1629;
 					}
-						
+					
 					if (complete > 0) {
 						sum += 543 + (complete * 341);
 					}
-						
+					
 					st.giveAdena(sum, true);
 				}
-					
+				
 				takeItems(player, -1, ZOMBIE_HEAD, ZOMBIE_HEART, ZOMBIE_LIVER, SKULL, RIB_BONE, SPINE, ARM_BONE, THIGH_BONE, COMPLETE_SKELETON);
-								
+				
 				if (event.equals("30434-06.html")) {
 					st.exitQuest(true, true);
 				}
diff --git a/src/main/resources/data/scripts.cfg b/src/main/resources/data/scripts.cfg
index 3ae6d83b77..ed637af580 100644
--- a/src/main/resources/data/scripts.cfg
+++ b/src/main/resources/data/scripts.cfg
@@ -8,6 +8,7 @@ com/l2jserver/datapack/features/SkillTransfer/SkillTransfer.java
 
 # Custom
 com/l2jserver/datapack/custom/Validators/SubClassSkills.java
+com/l2jserver/datapack/custom/service/buffer/BufferService.java
 
 # Custom Events
 com/l2jserver/datapack/custom/events/Elpies/Elpies.java
diff --git a/src/main/resources/data/service/buffer/html/community/category.html b/src/main/resources/data/service/buffer/html/community/category.html
new file mode 100644
index 0000000000..f8d6f8785c
--- /dev/null
+++ b/src/main/resources/data/service/buffer/html/community/category.html
@@ -0,0 +1,27 @@
+<html><title>%buffer.name% - Buffer</title><body><center>
+
+[INC(data/service/buffer/html/community/inc/header.html)ENDINC]
+
+<table width=570>
+	<tr>
+		<td><center>
+			<table width=200 cellspacing=0 cellpadding=0>
+				<tr><td width=32 height=32><button width=32 height=23 fore=L2UI_CT1.Button_DF back=L2UI_CT1.Button_DF_Down value="<<" action="%buffer.bypass_prefix% h m"></td><td width=200><center><font line="UNDERLINE" name="hs12" color=LEVEL>%category.name%</font></center></td></tr>
+			[FOREACH(buff IN category.buffs DO
+				[EXISTS(active_unique,
+					[IFCHILDS(active_unique.buffs, skill_id != buff.skill_id THEN
+						<tr><td height=35><img src="%buff.skill_icon%" width=32 height=32></td><td><center><button width=162 height=32 fore=L2UI_CT1.Button_DF back=L2UI_CT1.Button_DF_Down value="%buff.skill_name%" action="%buffer.bypass_prefix% u a %active_unique.name% %category.ident% %buff.ident%"></center></td></tr>
+					)ENDIFCHILDS]
+				)ENDEXISTS]
+				[EXISTS(!active_unique,
+					<tr><td height=35><img src="%buff.skill_icon%" width=32 height=32></td><td><center><button width=162 height=32 fore=L2UI_CT1.Button_DF back=L2UI_CT1.Button_DF_Down value="%buff.skill_name%" action="%buffer.bypass_prefix% t p b %category.ident% %buff.ident%"></center></td></tr>
+				)ENDEXISTS]
+			)ENDEACH]
+				<tr><td height=32></td><td></td></tr>
+			</table>
+		</center></td>
+		[INC(data/service/buffer/html/community/inc/active_unique_table.html)ENDINC]
+	</tr>
+</table>
+
+</center></body></html>
\ No newline at end of file
diff --git a/src/main/resources/data/service/buffer/html/community/inc/active_unique_table.html b/src/main/resources/data/service/buffer/html/community/inc/active_unique_table.html
new file mode 100644
index 0000000000..7cc3f585e1
--- /dev/null
+++ b/src/main/resources/data/service/buffer/html/community/inc/active_unique_table.html
@@ -0,0 +1,10 @@
+[EXISTS(active_unique,
+<td><center>
+	<table width=200 cellspacing=0 cellpadding=0>
+		<tr><td width=168><center><font line="UNDERLINE" name="hs12" color=LEVEL>%active_unique.name% (%active_unique.num_buffs%/%player.unique_max_buffs%, %active_unique.num_songs_dances%/%player.unique_max_songs_dances%)</font></center></td><td height=32><button width=32 height=23 fore=L2UI_CT1.Button_DF back=L2UI_CT1.Button_DF_Down value="Des." action="%buffer.bypass_prefix% u des"></td></tr>
+	[FOREACH(buff IN active_unique.buffs DO
+		<tr><td><center><button width=163 height=32 fore=L2UI_CT1.Button_DF back=L2UI_CT1.Button_DF_Down value="%buff.skill_name%" action="%buffer.bypass_prefix% u r %active_unique.name% %buff.ident%"></center></td><td height=35><img src="%buff.skill_icon%" width=32 height=32></td></tr>
+	)ENDEACH]
+	</table>
+</center></td>
+)ENDEXISTS]
diff --git a/src/main/resources/data/service/buffer/html/community/inc/header.html b/src/main/resources/data/service/buffer/html/community/inc/header.html
new file mode 100644
index 0000000000..17daeb3d9e
--- /dev/null
+++ b/src/main/resources/data/service/buffer/html/community/inc/header.html
@@ -0,0 +1,36 @@
+<table width=650 cellpadding=0 cellspacing=0>
+	<tr>
+		<td><combobox width=160 var="header_unique_selection" list="[FOREACH(unique IN uniques DO %unique.name%;)ENDEACH]" [EXISTS(active_unique, sel="%active_unique.name%")ENDEXISTS]></td>
+		<td><center><button width=100 height=23 fore=L2UI_CT1.Button_DF back=L2UI_CT1.Button_DF_Down value="Buff Me"  action="%buffer.bypass_prefix% t p u $header_unique_selection"></center></td>
+		<td><center><button width=100 height=23 fore=L2UI_CT1.Button_DF back=L2UI_CT1.Button_DF_Down value="Buff Summon"  action="%buffer.bypass_prefix% t s u $header_unique_selection"></center></td>
+		<td width=30></td>
+		<td><center><button width=115 height=23 fore=L2UI_CT1.Button_DF back=L2UI_CT1.Button_DF_Down value="Heal Me" action="%buffer.bypass_prefix% t p h"></center></td>
+		<td><center><button width=115 height=23 fore=L2UI_CT1.Button_DF back=L2UI_CT1.Button_DF_Down value="Heal Summon" action="%buffer.bypass_prefix% t s h"></center></td>
+	</tr>
+	<tr>
+		<td></td>
+		<td><center><button width=100 height=23 fore=L2UI_CT1.Button_DF back=L2UI_CT1.Button_DF_Down value="Select" action="%buffer.bypass_prefix% u s $header_unique_selection"></center></td>
+		<td><center><button width=100 height=23 fore=L2UI_CT1.Button_DF back=L2UI_CT1.Button_DF_Down value="Delete" action="%buffer.bypass_prefix% u del $header_unique_selection"></center></td>
+		<td></td>
+		<td><center><button width=115 height=23 fore=L2UI_CT1.Button_DF back=L2UI_CT1.Button_DF_Down value="Cancel Me" action="%buffer.bypass_prefix% t p c"></center></td>
+		<td><center><button width=115 height=23 fore=L2UI_CT1.Button_DF back=L2UI_CT1.Button_DF_Down value="Cancel Summon" action="%buffer.bypass_prefix% t s c"></center></td>
+	</tr>
+	<tr>
+		<td height=15></td><td></td><td></td><td></td><td></td><td></td>
+	</tr>
+	<tr>
+		<td><edit width=160 var="unique_new"></td>
+		<td><center><button width=100 height=23 fore="L2UI_CT1.Button_DF" back="L2UI_CT1.Button_DF_Down" value="Create Empty" action="%buffer.bypass_prefix% u c $unique_new"></center></td>
+		<td><center><button width=100 height=23 fore="L2UI_CT1.Button_DF" back="L2UI_CT1.Button_DF_Down" value="From Buffs" action="%buffer.bypass_prefix% u cfe $unique_new"></center></td>
+		<td></td>
+		<td></td>
+		<td></td>
+	</tr>
+</table><br><br>
+<font line="UNDERLINE" name="hs12" color=LEVEL>Presets</font><br1>
+<table width=400 cellspacing=0 cellpadding=0>
+	<tr>
+		<td><center><button width=195 height=23 fore="L2UI_CT1.Button_DF" back="L2UI_CT1.Button_DF_Down" value="Fighter" action="%buffer.bypass_prefix% t p p BC_FIGHTER"></center></td>
+		<td><center><button width=195 height=23 fore="L2UI_CT1.Button_DF" back="L2UI_CT1.Button_DF_Down" value="Mage" action="%buffer.bypass_prefix% t p p BC_MAGE"></center></td>
+	</tr>
+</table><br>
\ No newline at end of file
diff --git a/src/main/resources/data/service/buffer/html/community/main.html b/src/main/resources/data/service/buffer/html/community/main.html
new file mode 100644
index 0000000000..34aad84a9b
--- /dev/null
+++ b/src/main/resources/data/service/buffer/html/community/main.html
@@ -0,0 +1,20 @@
+<html><title>%buffer.name% - Buffer</title><body><center>
+
+[INC(data/service/buffer/html/community/inc/header.html)ENDINC]
+
+<table width=570>
+	<tr>
+		<td><center>
+			<table width=200 cellpadding=0 cellspacing=0>
+				<tr><td><center><font line="UNDERLINE" name="hs12" color=LEVEL>Categories</font></center></td></tr>
+				<tr><td height=15></td></tr>
+			[FOREACH(category IN buffer.categories DO
+				<tr><td><center><button width=195 height=23 fore=L2UI_CT1.Button_DF back=L2UI_CT1.Button_DF_Down value="%category.name%" action="%buffer.bypass_prefix% h c %category.ident%"></center></td></tr>
+			)ENDEACH]
+			</table>
+		</center></td>
+		[INC(data/service/buffer/html/community/inc/active_unique_table.html)ENDINC]
+	</tr>
+</table>
+
+</center></body></html>
\ No newline at end of file
diff --git a/src/main/resources/data/service/buffer/html/npc/category.html b/src/main/resources/data/service/buffer/html/npc/category.html
new file mode 100644
index 0000000000..a9908ac1e1
--- /dev/null
+++ b/src/main/resources/data/service/buffer/html/npc/category.html
@@ -0,0 +1,42 @@
+<html><title>%buffer.name% - Buffer</title><body>
+
+<a action="%buffer.bypass_prefix%">Home</a>&nbsp;> Category > <font color=LEVEL>%category.name%</font><br><center>
+
+[EXISTS(!active_unique,
+	<table width=270 cellspacing=0 cellpadding=0>
+		<tr>
+			<td><center><combobox width=130 var="unique_selection" list="[FOREACH(unique IN uniques DO %unique.name%;)ENDEACH]"></center></td>
+			<td><center><button width=130 height=23 fore=L2UI_CT1.Button_DF back=L2UI_CT1.Button_DF_Down value="Select" action="%buffer.bypass_prefix% u s $unique_selection"></center></td>
+		</tr>
+	</table><br>
+)ENDEXISTS]
+[EXISTS(active_unique,
+	<table width=270 cellspacing=0 cellpadding=0>
+		<tr>
+			<td width=135><center><font color=LEVEL>%active_unique.name%</font> (%active_unique.num_buffs%/%player.unique_max_buffs%, %active_unique.num_songs_dances%/%player.unique_max_songs_dances%)</font></center></td>
+			<td><center><button width=130 height=23 fore=L2UI_CT1.Button_DF back=L2UI_CT1.Button_DF_Down value="Deselect" action="%buffer.bypass_prefix% u des"></center></center></td>
+		</tr>
+	</table><br>	
+)ENDEXISTS]
+
+<table width=270 cellspacing=0 cellpadding=0>
+	[FOREACH(buff IN category.buffs DO
+		[EXISTS(active_unique,
+			[IFCHILDS(active_unique.buffs, skill_id != buff.skill_id THEN
+			<tr>
+				<td width=32 height=32><img width=32 height=32 src="%buff.skill_icon%"></td>
+				<td><button width=238 height=32 fore=L2UI_CT1.Button_DF back=L2UI_CT1.Button_DF_Down value="%buff.skill_name%" action="%buffer.bypass_prefix% u a %active_unique.name% %category.ident% %buff.ident%"></td>
+			</tr>
+			<tr><td height=10></td><td></td></tr>
+			)ENDIFCHILDS]
+		)ENDEXISTS]
+		[EXISTS(!active_unique,
+		<tr>
+			<td width=32 height=32><img width=32 height=32 src="%buff.skill_icon%"></td>
+			<td><button width=238 height=32 fore=L2UI_CT1.Button_DF back=L2UI_CT1.Button_DF_Down value="%buff.skill_name%" action="%buffer.bypass_prefix% t p b %category.ident% %buff.ident%"></td>
+		</tr>
+		<tr><td height=10></td><td></td></tr>
+		)ENDEXISTS]
+	)ENDEACH]
+</table>
+</center></body></html>
\ No newline at end of file
diff --git a/src/main/resources/data/service/buffer/html/npc/main.html b/src/main/resources/data/service/buffer/html/npc/main.html
new file mode 100644
index 0000000000..f229dfe1b6
--- /dev/null
+++ b/src/main/resources/data/service/buffer/html/npc/main.html
@@ -0,0 +1,67 @@
+<html><title>%buffer.name% - Buffer</title><body><center>
+
+<table width=270 cellspacing=0 cellpadding=0>
+	<tr>
+		<td><center><button width=130 height=23 fore=L2UI_CT1.Button_DF back=L2UI_CT1.Button_DF_Down value="Heal Me" action="%buffer.bypass_prefix% t p h"></center></td>
+		<td><center><button width=130 height=23 fore=L2UI_CT1.Button_DF back=L2UI_CT1.Button_DF_Down value="Heal Summon" action="%buffer.bypass_prefix% t s h"></center></td>
+	</tr>
+</table>
+
+<table width=270 cellspacing=0 cellpadding=0>
+	<tr>
+		<td><center><button width=130 height=23 fore=L2UI_CT1.Button_DF back=L2UI_CT1.Button_DF_Down value="Cancel Me" action="%buffer.bypass_prefix% t p c"></center></td>
+		<td><center><button width=130 height=23 fore=L2UI_CT1.Button_DF back=L2UI_CT1.Button_DF_Down value="Cancel Summon" action="%buffer.bypass_prefix% t s c"></center></td>
+	</tr>
+</table><br>
+
+<font name=hs12 line=UNDERLINE color=LEVEL>Presets</font><br1>
+<table width=270 cellspacing=0 cellpadding=0>
+	<tr>
+		<td><center><button width=130 height=23 fore=L2UI_CT1.Button_DF back=L2UI_CT1.Button_DF_Down value="Fighter" action="%buffer.bypass_prefix% t p p BC_FIGHTER"></center></td>
+		<td><center><button width=130 height=23 fore=L2UI_CT1.Button_DF back=L2UI_CT1.Button_DF_Down value="Mage" action="%buffer.bypass_prefix% t p p BC_MAGE"></center></td>
+	</tr>
+</table><br>
+
+<font name=hs12 line=UNDERLINE color=LEVEL>Categories</font><br1>
+<table width=270 cellspacing=0 cellpadding=0>
+	<tr>
+		<td><center><button width=130 height=23 fore=L2UI_CT1.Button_DF back=L2UI_CT1.Button_DF_Down value="Buffs" action="%buffer.bypass_prefix% h c BC_BUFFS"></center></td>
+		<td><center><button width=130 height=23 fore=L2UI_CT1.Button_DF back=L2UI_CT1.Button_DF_Down value="Songs" action="%buffer.bypass_prefix% h c BC_SONGS"></center></td>
+	</tr>
+	<tr>
+		<td><center><button width=130 height=23 fore=L2UI_CT1.Button_DF back=L2UI_CT1.Button_DF_Down value="Dances" action="%buffer.bypass_prefix% h c BC_DANCES"></center></td>
+		<td><center><button width=130 height=23 fore=L2UI_CT1.Button_DF back=L2UI_CT1.Button_DF_Down value="Chants" action="%buffer.bypass_prefix% h c BC_CHANTS"></center></td>
+	</tr>
+	<tr>
+		<td><center><button width=130 height=23 fore=L2UI_CT1.Button_DF back=L2UI_CT1.Button_DF_Down value="Dwarven" action="%buffer.bypass_prefix% h c BC_DWARVEN"></center></td>
+		<td><center><button width=130 height=23 fore=L2UI_CT1.Button_DF back=L2UI_CT1.Button_DF_Down value="Resist" action="%buffer.bypass_prefix% h c BC_RESIST"></center></td>
+	</tr>
+	<tr>
+		<td><center><button width=130 height=23 fore=L2UI_CT1.Button_DF back=L2UI_CT1.Button_DF_Down value="Special" action="%buffer.bypass_prefix% h c BC_SPECIAL"></center></td>
+		<td><center><button width=130 height=23 fore=L2UI_CT1.Button_DF back=L2UI_CT1.Button_DF_Down value="Overlord" action="%buffer.bypass_prefix% h c BC_OVERLORD"></center></td>
+	</tr>
+</table><br>
+
+<font name=hs12 line=UNDERLINE color=LEVEL>Unique</font><br1>
+<table width=270 cellspacing=0 cellpadding=0>
+	<tr>
+		<td><center><combobox width=85 var="unique_selection" list="[FOREACH(unique IN uniques DO %unique.name%;)ENDEACH]"></center></td>
+		<td><center><button width=85 height=23 fore=L2UI_CT1.Button_DF back=L2UI_CT1.Button_DF_Down value="Buff Me" action="%buffer.bypass_prefix% t p u $unique_selection"></center></td>
+		<td><center><button width=85 height=23 fore=L2UI_CT1.Button_DF back=L2UI_CT1.Button_DF_Down value="Buff Summon" action="%buffer.bypass_prefix% t s u $unique_selection"></center></td>
+	</tr>
+	<tr>
+		<td></td>
+		<td><center><button width=85 height=23 fore=L2UI_CT1.Button_DF back=L2UI_CT1.Button_DF_Down value="Inspect" action="%buffer.bypass_prefix% h u $unique_selection"></center></td>
+		<td><center><button width=85 height=23 fore=L2UI_CT1.Button_DF back=L2UI_CT1.Button_DF_Down value="Delete" action="%buffer.bypass_prefix% u del $unique_selection"></center></td>
+	</tr>
+	<tr>
+		<td height=15></td><td></td><td></td>
+	</tr>
+	<tr>
+		<td><center><edit width=85 var="unique_new"></center></td>
+		<td><center><button width=85 height=23 fore=L2UI_CT1.Button_DF back=L2UI_CT1.Button_DF_Down value="Create Empty" action="%buffer.bypass_prefix% u c $unique_new"></center></td>
+		<td><center><button width=85 height=23 fore=L2UI_CT1.Button_DF back=L2UI_CT1.Button_DF_Down value="From Buffs" action="%buffer.bypass_prefix% u cfe $unique_new"></center></td>
+	</tr>
+</table>
+
+</center></body></html>
\ No newline at end of file
diff --git a/src/main/resources/data/service/buffer/html/npc/unique.html b/src/main/resources/data/service/buffer/html/npc/unique.html
new file mode 100644
index 0000000000..5f969b3189
--- /dev/null
+++ b/src/main/resources/data/service/buffer/html/npc/unique.html
@@ -0,0 +1,17 @@
+<html><title>%buffer.name% - Buffer</title><body>
+
+<a action="%buffer.bypass_prefix%">Home</a>&nbsp;> Unique > <font color=LEVEL>%unique.name%</font> (%unique.num_buffs%/%player.unique_max_buffs%, %unique.num_songs_dances%/%player.unique_max_songs_dances%)<br>
+
+<center>
+	<table width=270 cellspacing=0 cellpadding=0>
+		[FOREACH(buff IN unique.buffs DO
+		<tr>
+			<td width=32 height=32><img width=32 height=32 src="%buff.skill_icon%"></td>
+			<td><button width=238 height=32 fore=L2UI_CT1.Button_DF back=L2UI_CT1.Button_DF_Down value="%buff.skill_name%" action="%buffer.bypass_prefix% u r %unique.name% %buff.ident%"></td>
+		</tr>
+		<tr><td height=10></td><td></td></tr>
+		)ENDEACH]
+	</table>
+</center>
+
+</body></html>
\ No newline at end of file
diff --git a/src/main/resources/data/service/buffer/json/documentation.txt b/src/main/resources/data/service/buffer/json/documentation.txt
new file mode 100644
index 0000000000..ec416f6306
--- /dev/null
+++ b/src/main/resources/data/service/buffer/json/documentation.txt
@@ -0,0 +1,85 @@
+################################################################################
+# BufferService - JSON Docs                                                    #
+################################################################################
+
+###############
+# global.json #
+###############
+
+    {
+        "buffs": {
+            "<buffId>": {
+                "id":    "<buffId>",
+                "skill": (number),
+                "level": (number),
+                "type":  "BUFF" or "SONG_DANCE",
+                "items" : [
+                    { "id":(number), "amount":(number) },
+                    ....
+                ]
+            },
+            ....
+        },
+
+        "buffCategories": {
+            "<buffCategoryId>": {
+                "id":   "<buffCategoryId>",
+                "name": (string),
+                "buffs": ["<buffId>", ...]
+            },
+            ....
+        }
+    }
+
+    Notes:
+    • buffs: An object where each property represents a buff (the "id" property must match the key in "buffs").
+    • buffs.<buffId>.skill: skill id
+    • buffs.<buffId>.level: skill level
+    • buffs.<buffId>.type: This property must be a string of either "BUFF" or "SONG_DANCE".
+    • buffs.<buffId>.items: An array of item objects
+    • buffCategories: An object where each property represents a buff category (the "id" property must match the key in "buffCategories"
+    • buffCategories.<buffCategoryId>.name: display name of buff category
+    • buffCategories.<buffCategoryId>.buffs: property is an array of <buffId> from global.json
+
+
+###############
+# voiced.json #
+###############
+
+    {
+        "dialogType": "NPC" or "COMMUNITY",
+        "htmlFolder": (string),
+        "canHeal": (boolean),
+        "canCancel": (boolean),
+        
+        "presetBuffCategories": ["<buffCategoryId>", ...],
+        "buffCategories": ["<buffCategoryId>", ...]
+    }
+
+    Notes:
+    • dialogType: NPC opens npc html window, COMMUNITY opens community board
+    • htmlFolder: from where to load html files
+    • canHeal: whatever this buffer can heal
+    • canCancel: whatever this buffer can cancel buffs
+    • presetBuffCategories & buffCategories: both properties are an array of <buffCategoryId> from global.json
+
+#####################
+# npcs/<npcId>.json #
+#####################
+
+    {
+        "npcId": "<npcId>",
+        "directFirstTalk": (boolean),
+        "dialogType": "NPC" or "COMMUNITY",
+        "htmlFolder": (string),
+        "canHeal": (boolean),
+        "canCancel": (boolean),
+        
+        "presetBuffCategories": ["<buffCategoryId>", ...],
+        "buffCategories": ["<buffCategoryId>", ...]
+    }
+
+    Notes:
+    • npcId: the npc id
+    • directFirstTalk: whatever to directly show the script htmls (true) or initially show htmls from <datapack>/data/html when talking to the npc by clicking on it
+    • see notes from voiced.json
\ No newline at end of file
diff --git a/src/main/resources/data/service/buffer/json/global.json b/src/main/resources/data/service/buffer/json/global.json
new file mode 100644
index 0000000000..f66e93ba6f
--- /dev/null
+++ b/src/main/resources/data/service/buffer/json/global.json
@@ -0,0 +1,197 @@
+{
+    "buffs": {
+        "BUFF_0": {"id":"BUFF_0", "skill":1036, "level":2, "type":"BUFF","items":[{"id":57, "amount":100}]},
+		"BUFF_1": {"id":"BUFF_1", "skill":1040, "level":3, "type":"BUFF", "items":[{"id":57, "amount":100}]},
+		"BUFF_2": {"id":"BUFF_2", "skill":1043, "level":1, "type":"BUFF", "items":[{"id":57, "amount":100}]},
+		"BUFF_3": {"id":"BUFF_3", "skill":1044, "level":3, "type":"BUFF", "items":[{"id":57, "amount":100}]},
+		"BUFF_4": {"id":"BUFF_4", "skill":1045, "level":6, "type":"BUFF", "items":[{"id":57, "amount":100}]},
+		"BUFF_5": {"id":"BUFF_5", "skill":1047, "level":4, "type":"BUFF", "items":[{"id":57, "amount":100}]},
+		"BUFF_6": {"id":"BUFF_6", "skill":1048, "level":2, "type":"BUFF", "items":[{"id":57, "amount":100}]},
+		"BUFF_7": {"id":"BUFF_7", "skill":1059, "level":6, "type":"BUFF", "items":[{"id":57, "amount":100}]},
+		"BUFF_8": {"id":"BUFF_8", "skill":1068, "level":3, "type":"BUFF", "items":[{"id":57, "amount":100}]},
+		"BUFF_9": {"id":"BUFF_9", "skill":1077, "level":3, "type":"BUFF", "items":[{"id":57, "amount":100}]},
+		"BUFF_10": {"id":"BUFF_10", "skill":1085, "level":3, "type":"BUFF", "items":[{"id":57, "amount":100}]},
+		"BUFF_11": {"id":"BUFF_11", "skill":1086, "level":2, "type":"BUFF", "items":[{"id":57, "amount":100}]},
+		"BUFF_12": {"id":"BUFF_12", "skill":1087, "level":3, "type":"BUFF", "items":[{"id":57, "amount":100}]},
+		"BUFF_13": {"id":"BUFF_13", "skill":1204, "level":2, "type":"BUFF", "items":[{"id":57, "amount":100}]},
+		"BUFF_14": {"id":"BUFF_14", "skill":1242, "level":3, "type":"BUFF", "items":[{"id":57, "amount":100}]},
+		"BUFF_15": {"id":"BUFF_15", "skill":1243, "level":6, "type":"BUFF", "items":[{"id":57, "amount":100}]},
+		"BUFF_16": {"id":"BUFF_16", "skill":1257, "level":3, "type":"BUFF", "items":[{"id":57, "amount":100}]},
+		"BUFF_17": {"id":"BUFF_17", "skill":1268, "level":4, "type":"BUFF", "items":[{"id":57, "amount":100}]},
+		"BUFF_18": {"id":"BUFF_18", "skill":1303, "level":2, "type":"BUFF", "items":[{"id":57, "amount":100}]},
+		"BUFF_19": {"id":"BUFF_19", "skill":1304, "level":3, "type":"BUFF", "items":[{"id":57, "amount":100}]},
+		"BUFF_20": {"id":"BUFF_20", "skill":1307, "level":3, "type":"BUFF", "items":[{"id":57, "amount":100}]},
+		"BUFF_21": {"id":"BUFF_21", "skill":1311, "level":6, "type":"BUFF", "items":[{"id":57, "amount":100}]},
+		"BUFF_22": {"id":"BUFF_22", "skill":1397, "level":3, "type":"BUFF", "items":[{"id":57, "amount":100}]},
+		"BUFF_23": {"id":"BUFF_23", "skill":1460, "level":1, "type":"BUFF", "items":[{"id":57, "amount":100}]},
+		"BUFF_24": {"id":"BUFF_24", "skill":1240, "level":3, "type":"BUFF", "items":[{"id":57, "amount":100}]},
+
+        "SONG_0": {"id":"SONG_0", "skill":264, "level":1, "type":"SONG_DANCE", "items":[{"id":57, "amount":100}]},
+		"SONG_1": {"id":"SONG_1", "skill":265, "level":1, "type":"SONG_DANCE", "items":[{"id":57, "amount":100}]},
+		"SONG_2": {"id":"SONG_2", "skill":266, "level":1, "type":"SONG_DANCE", "items":[{"id":57, "amount":100}]},
+		"SONG_3": {"id":"SONG_3", "skill":267, "level":1, "type":"SONG_DANCE", "items":[{"id":57, "amount":100}]},
+		"SONG_4": {"id":"SONG_4", "skill":268, "level":1, "type":"SONG_DANCE", "items":[{"id":57, "amount":100}]},
+		"SONG_5": {"id":"SONG_5", "skill":269, "level":1, "type":"SONG_DANCE", "items":[{"id":57, "amount":100}]},
+		"SONG_6": {"id":"SONG_6", "skill":270, "level":1, "type":"SONG_DANCE", "items":[{"id":57, "amount":100}]},
+		"SONG_7": {"id":"SONG_7", "skill":304, "level":1, "type":"SONG_DANCE", "items":[{"id":57, "amount":100}]},
+		"SONG_8": {"id":"SONG_8", "skill":305, "level":1, "type":"SONG_DANCE", "items":[{"id":57, "amount":100}]},
+		"SONG_9": {"id":"SONG_9", "skill":306, "level":1, "type":"SONG_DANCE", "items":[{"id":57, "amount":100}]},
+		"SONG_10": {"id":"SONG_10", "skill":308, "level":1, "type":"SONG_DANCE", "items":[{"id":57, "amount":100}]},
+		"SONG_11": {"id":"SONG_11", "skill":349, "level":1, "type":"SONG_DANCE", "items":[{"id":57, "amount":100}]},
+		"SONG_12": {"id":"SONG_12", "skill":363, "level":1, "type":"SONG_DANCE", "items":[{"id":57, "amount":100}]},
+		"SONG_13": {"id":"SONG_13", "skill":364, "level":1, "type":"SONG_DANCE", "items":[{"id":57, "amount":100}]},
+		"SONG_14": {"id":"SONG_14", "skill":529, "level":1, "type":"SONG_DANCE", "items":[{"id":57, "amount":100}]},
+		"SONG_15": {"id":"SONG_15", "skill":764, "level":1, "type":"SONG_DANCE", "items":[{"id":57, "amount":100}]},
+
+        "DANCE_0": {"id":"DANCE_0", "skill":271, "level":1, "type":"SONG_DANCE", "items":[{"id":57, "amount":100}]},
+		"DANCE_1": {"id":"DANCE_1", "skill":272, "level":1, "type":"SONG_DANCE", "items":[{"id":57, "amount":100}]},
+		"DANCE_2": {"id":"DANCE_2", "skill":273, "level":1, "type":"SONG_DANCE", "items":[{"id":57, "amount":100}]},
+		"DANCE_3": {"id":"DANCE_3", "skill":274, "level":1, "type":"SONG_DANCE", "items":[{"id":57, "amount":100}]},
+		"DANCE_4": {"id":"DANCE_4", "skill":275, "level":1, "type":"SONG_DANCE", "items":[{"id":57, "amount":100}]},
+		"DANCE_5": {"id":"DANCE_5", "skill":276, "level":1, "type":"SONG_DANCE", "items":[{"id":57, "amount":100}]},
+		"DANCE_6": {"id":"DANCE_6", "skill":277, "level":1, "type":"SONG_DANCE", "items":[{"id":57, "amount":100}]},
+		"DANCE_7": {"id":"DANCE_7", "skill":307, "level":1, "type":"SONG_DANCE", "items":[{"id":57, "amount":100}]},
+		"DANCE_8": {"id":"DANCE_8", "skill":309, "level":1, "type":"SONG_DANCE", "items":[{"id":57, "amount":100}]},
+		"DANCE_9": {"id":"DANCE_9", "skill":310, "level":1, "type":"SONG_DANCE", "items":[{"id":57, "amount":100}]},
+		"DANCE_10": {"id":"DANCE_10", "skill":311, "level":1, "type":"SONG_DANCE", "items":[{"id":57, "amount":100}]},
+		"DANCE_11": {"id":"DANCE_11", "skill":365, "level":1, "type":"SONG_DANCE", "items":[{"id":57, "amount":100}]},
+		"DANCE_12": {"id":"DANCE_12", "skill":366, "level":1, "type":"SONG_DANCE", "items":[{"id":57, "amount":100}]},
+		"DANCE_13": {"id":"DANCE_13", "skill":530, "level":1, "type":"SONG_DANCE", "items":[{"id":57, "amount":100}]},
+		"DANCE_14": {"id":"DANCE_14", "skill":765, "level":1, "type":"SONG_DANCE", "items":[{"id":57, "amount":100}]},
+		"DANCE_15": {"id":"DANCE_15", "skill":915, "level":1, "type":"SONG_DANCE", "items":[{"id":57, "amount":100}]},
+
+        "CHANT_0": {"id":"CHANT_0", "skill":1002, "level":3, "type":"BUFF", "items":[{"id":57, "amount":100}]},
+		"CHANT_1": {"id":"CHANT_1", "skill":1006, "level":3, "type":"BUFF", "items":[{"id":57, "amount":100}]},
+		"CHANT_2": {"id":"CHANT_2", "skill":1007, "level":3, "type":"BUFF", "items":[{"id":57, "amount":100}]},
+		"CHANT_3": {"id":"CHANT_3", "skill":1009, "level":3, "type":"BUFF", "items":[{"id":57, "amount":100}]},
+		"CHANT_4": {"id":"CHANT_4", "skill":1251, "level":3, "type":"BUFF", "items":[{"id":57, "amount":100}]},
+		"CHANT_5": {"id":"CHANT_5", "skill":1252, "level":3, "type":"BUFF", "items":[{"id":57, "amount":100}]},
+		"CHANT_6": {"id":"CHANT_6", "skill":1253, "level":3, "type":"BUFF", "items":[{"id":57, "amount":100}]},
+		"CHANT_7": {"id":"CHANT_7", "skill":1284, "level":3, "type":"BUFF", "items":[{"id":57, "amount":100}]},
+		"CHANT_8": {"id":"CHANT_8", "skill":1308, "level":3, "type":"BUFF", "items":[{"id":57, "amount":100}]},
+		"CHANT_9": {"id":"CHANT_9", "skill":1309, "level":3, "type":"BUFF", "items":[{"id":57, "amount":100}]},
+		"CHANT_10": {"id":"CHANT_10", "skill":1310, "level":4, "type":"BUFF", "items":[{"id":57, "amount":100}]},
+		"CHANT_11": {"id":"CHANT_11", "skill":1362, "level":1, "type":"BUFF", "items":[{"id":57, "amount":100}]},
+
+        "DWARVEN_0": {"id":"DWARVEN_0", "skill":825, "level":1, "type":"BUFF", "items":[{"id":57, "amount":100}]},
+		"DWARVEN_1": {"id":"DWARVEN_1", "skill":826, "level":1, "type":"BUFF", "items":[{"id":57, "amount":100}]},
+		"DWARVEN_2": {"id":"DWARVEN_2", "skill":827, "level":1, "type":"BUFF", "items":[{"id":57, "amount":100}]},
+		"DWARVEN_3": {"id":"DWARVEN_3", "skill":828, "level":1, "type":"BUFF", "items":[{"id":57, "amount":100}]},
+		"DWARVEN_4": {"id":"DWARVEN_4", "skill":829, "level":1, "type":"BUFF", "items":[{"id":57, "amount":100}]},
+		"DWARVEN_5": {"id":"DWARVEN_5", "skill":830, "level":1, "type":"BUFF", "items":[{"id":57, "amount":100}]},
+
+        "RESIST_0": {"id":"RESIST_0", "skill":1461, "level":1, "type":"BUFF", "items":[{"id":57, "amount":100}]},
+		"RESIST_1": {"id":"RESIST_1", "skill":1032, "level":3, "type":"BUFF", "items":[{"id":57, "amount":100}]},
+		"RESIST_2": {"id":"RESIST_2", "skill":1033, "level":3, "type":"BUFF", "items":[{"id":57, "amount":100}]},
+		"RESIST_3": {"id":"RESIST_3", "skill":1035, "level":4, "type":"BUFF", "items":[{"id":57, "amount":100}]},
+		"RESIST_4": {"id":"RESIST_4", "skill":1078, "level":6, "type":"BUFF", "items":[{"id":57, "amount":100}]},
+		"RESIST_5": {"id":"RESIST_5", "skill":1182, "level":3, "type":"BUFF", "items":[{"id":57, "amount":100}]},
+		"RESIST_6": {"id":"RESIST_6", "skill":1189, "level":3, "type":"BUFF", "items":[{"id":57, "amount":100}]},
+		"RESIST_7": {"id":"RESIST_7", "skill":1191, "level":3, "type":"BUFF", "items":[{"id":57, "amount":100}]},
+		"RESIST_8": {"id":"RESIST_8", "skill":1259, "level":4, "type":"BUFF", "items":[{"id":57, "amount":100}]},
+		"RESIST_9": {"id":"RESIST_9", "skill":1352, "level":1, "type":"BUFF", "items":[{"id":57, "amount":100}]},
+		"RESIST_10": {"id":"RESIST_10", "skill":1353, "level":1, "type":"BUFF", "items":[{"id":57, "amount":100}]},
+		"RESIST_11": {"id":"RESIST_11", "skill":1354, "level":1, "type":"BUFF", "items":[{"id":57, "amount":100}]},
+		"RESIST_12": {"id":"RESIST_12", "skill":1392, "level":3, "type":"BUFF", "items":[{"id":57, "amount":100}]},
+		"RESIST_13": {"id":"RESIST_13", "skill":1393, "level":3, "type":"BUFF", "items":[{"id":57, "amount":100}]},
+		"RESIST_14": {"id":"RESIST_14", "skill":1548, "level":3, "type":"BUFF", "items":[{"id":57, "amount":100}]},
+
+        "SPECIAL_0": {"id":"SPECIAL_0", "skill":1388, "level":3, "type":"BUFF", "items":[{"id":57, "amount":100}]},
+		"SPECIAL_1": {"id":"SPECIAL_1", "skill":1389, "level":3, "type":"BUFF", "items":[{"id":57, "amount":100}]},
+		"SPECIAL_2": {"id":"SPECIAL_2", "skill":1499, "level":1, "type":"BUFF", "items":[{"id":57, "amount":100}]},
+		"SPECIAL_3": {"id":"SPECIAL_3", "skill":1500, "level":1, "type":"BUFF", "items":[{"id":57, "amount":100}]},
+		"SPECIAL_4": {"id":"SPECIAL_4", "skill":1501, "level":1, "type":"BUFF", "items":[{"id":57, "amount":100}]},
+		"SPECIAL_5": {"id":"SPECIAL_5", "skill":1502, "level":1, "type":"BUFF", "items":[{"id":57, "amount":100}]},
+		"SPECIAL_6": {"id":"SPECIAL_6", "skill":1503, "level":1, "type":"BUFF", "items":[{"id":57, "amount":100}]},
+		"SPECIAL_7": {"id":"SPECIAL_7", "skill":1504, "level":1, "type":"BUFF", "items":[{"id":57, "amount":100}]},
+		"SPECIAL_8": {"id":"SPECIAL_8", "skill":1519, "level":1, "type":"BUFF", "items":[{"id":57, "amount":100}]},
+		"SPECIAL_9": {"id":"SPECIAL_9", "skill":1062, "level":2, "type":"BUFF", "items":[{"id":57, "amount":100}]},
+		"SPECIAL_10": {"id":"SPECIAL_10", "skill":1355, "level":1, "type":"BUFF", "items":[{"id":57, "amount":100}]},
+		"SPECIAL_11": {"id":"SPECIAL_11", "skill":1356, "level":1, "type":"BUFF", "items":[{"id":57, "amount":100}]},
+		"SPECIAL_12": {"id":"SPECIAL_12", "skill":1357, "level":1, "type":"BUFF", "items":[{"id":57, "amount":100}]},
+		"SPECIAL_13": {"id":"SPECIAL_13", "skill":1363, "level":1, "type":"BUFF", "items":[{"id":57, "amount":100}]},
+		"SPECIAL_14": {"id":"SPECIAL_14", "skill":1413, "level":1, "type":"BUFF", "items":[{"id":57, "amount":100}]},
+		"SPECIAL_15": {"id":"SPECIAL_15", "skill":1261, "level":2, "type":"BUFF", "items":[{"id":57, "amount":100}]},
+		"SPECIAL_16": {"id":"SPECIAL_16", "skill":1415, "level":1, "type":"BUFF", "items":[{"id":57, "amount":100}]},
+		"SPECIAL_17": {"id":"SPECIAL_17", "skill":1416, "level":1, "type":"BUFF", "items":[{"id":57, "amount":100}]},
+		"SPECIAL_18": {"id":"SPECIAL_18", "skill":1542, "level":1, "type":"BUFF", "items":[{"id":57, "amount":100}]},
+		"SPECIAL_19": {"id":"SPECIAL_19", "skill":1414, "level":1, "type":"BUFF", "items":[{"id":57, "amount":100}]},
+		"SPECIAL_20": {"id":"SPECIAL_20", "skill":4699, "level":13, "type":"BUFF", "items":[{"id":57, "amount":100}]},
+		"SPECIAL_21": {"id":"SPECIAL_21", "skill":4700, "level":13, "type":"BUFF", "items":[{"id":57, "amount":100}]},
+		"SPECIAL_22": {"id":"SPECIAL_22", "skill":4702, "level":13, "type":"BUFF", "items":[{"id":57, "amount":100}]},
+		"SPECIAL_23": {"id":"SPECIAL_23", "skill":4703, "level":13, "type":"BUFF", "items":[{"id":57, "amount":100}]},
+
+        "OVERLORD_0": {"id":"OVERLORD_0", "skill":1003, "level":3, "type":"BUFF", "items":[{"id":57, "amount":100}]},
+		"OVERLORD_1": {"id":"OVERLORD_1", "skill":1004, "level":3, "type":"BUFF", "items":[{"id":57, "amount":100}]},
+		"OVERLORD_2": {"id":"OVERLORD_2", "skill":1005, "level":3, "type":"BUFF", "items":[{"id":57, "amount":100}]},
+		"OVERLORD_3": {"id":"OVERLORD_3", "skill":1008, "level":3, "type":"BUFF", "items":[{"id":57, "amount":100}]},
+		"OVERLORD_4": {"id":"OVERLORD_4", "skill":1249, "level":3, "type":"BUFF", "items":[{"id":57, "amount":100}]},
+		"OVERLORD_5": {"id":"OVERLORD_5", "skill":1250, "level":3, "type":"BUFF", "items":[{"id":57, "amount":100}]},
+		"OVERLORD_6": {"id":"OVERLORD_6", "skill":1260, "level":3, "type":"BUFF", "items":[{"id":57, "amount":100}]},
+		"OVERLORD_7": {"id":"OVERLORD_7", "skill":1282, "level":2, "type":"BUFF", "items":[{"id":57, "amount":100}]},
+		"OVERLORD_8": {"id":"OVERLORD_8", "skill":1364, "level":1, "type":"BUFF", "items":[{"id":57, "amount":100}]},
+		"OVERLORD_9": {"id":"OVERLORD_9", "skill":1365, "level":1, "type":"BUFF", "items":[{"id":57, "amount":100}]}
+	},
+
+    "buffCategories": {
+        "BC_FIGHTER": {
+			"id":"BC_FIGHTER",
+            "name":"Fighter",
+            "buffs":["DANCE_0", "DANCE_1", "DANCE_3", "DANCE_4", "DANCE_5", "DANCE_6", "DANCE_7", "DANCE_8", "DANCE_9", "DANCE_10", "DANCE_13", "DANCE_15", "SONG_0", "SONG_1", "SONG_2", "SONG_3", "SONG_4", "SONG_5", "SONG_6", "SONG_7", "SONG_8", "SONG_9", "SONG_10", "SONG_11", "SONG_12", "SONG_13", "SONG_14", "SONG_15", "BUFF_3", "BUFF_11", "BUFF_16", "BUFF_17", "BUFF_20", "BUFF_22", "BUFF_24", "RESIST_3", "RESIST_8", "RESIST_9", "RESIST_10", "RESIST_11", "SPECIAL_0", "SPECIAL_2", "SPECIAL_3", "SPECIAL_4", "SPECIAL_5", "SPECIAL_6", "SPECIAL_7", "SPECIAL_9", "SPECIAL_18"]
+        },
+
+        "BC_MAGE": {
+			"id":"BC_MAGE",
+            "name":"Mage",
+            "buffs":["DANCE_2", "DANCE_5", "DANCE_7", "DANCE_8", "DANCE_10", "DANCE_11", "DANCE_13", "DANCE_15", "SONG_1", "SONG_2", "SONG_3", "SONG_4", "SONG_5", "SONG_6", "SONG_7", "SONG_8", "SONG_9", "SONG_10", "SONG_11", "SONG_12", "SONG_13", "SONG_14", "SONG_15", "BUFF_3", "BUFF_10", "BUFF_11", "BUFF_16", "BUFF_18", "BUFF_20", "BUFF_22", "RESIST_4", "RESIST_5", "RESIST_6", "RESIST_7", "RESIST_8", "RESIST_9", "RESIST_10", "RESIST_11", "RESIST_12","RESIST_13", "RESIST_14", "SPECIAL_1", "SPECIAL_2", "SPECIAL_3", "SPECIAL_4", "SPECIAL_6"]
+		},
+
+        "BC_BUFFS": {
+			"id":"BC_BUFFS",
+            "name":"Buffs",
+            "buffs":["BUFF_0", "BUFF_1", "BUFF_2", "BUFF_3", "BUFF_4", "BUFF_5", "BUFF_6", "BUFF_7", "BUFF_8", "BUFF_9", "BUFF_10", "BUFF_11", "BUFF_12", "BUFF_13", "BUFF_14", "BUFF_15", "BUFF_16", "BUFF_17", "BUFF_18", "BUFF_19", "BUFF_20", "BUFF_21", "BUFF_22", "BUFF_23", "BUFF_24"]
+        },
+		
+		"BC_SONGS": {
+			"id":"BC_SONGS",
+            "name":"Songs",
+            "buffs":["SONG_0", "SONG_1", "SONG_2", "SONG_3", "SONG_4", "SONG_5", "SONG_6", "SONG_7", "SONG_8", "SONG_9", "SONG_10", "SONG_11", "SONG_12", "SONG_13", "SONG_14", "SONG_15"]
+        },
+		
+		"BC_DANCES": {
+			"id":"BC_DANCES",
+            "name":"Dances",
+            "buffs":["DANCE_0", "DANCE_1", "DANCE_2", "DANCE_3", "DANCE_4", "DANCE_5", "DANCE_6", "DANCE_7", "DANCE_8", "DANCE_9", "DANCE_10", "DANCE_11", "DANCE_12", "DANCE_13", "DANCE_14", "DANCE_15"]
+        },
+		
+		"BC_CHANTS": {
+			"id":"BC_CHANTS",
+            "name":"Chants",
+            "buffs":["CHANT_0", "CHANT_1", "CHANT_2", "CHANT_3", "CHANT_4", "CHANT_5", "CHANT_6", "CHANT_7", "CHANT_8", "CHANT_9", "CHANT_10", "CHANT_11"]
+        },
+		
+		"BC_DWARVEN": {
+			"id":"BC_DWARVEN",
+            "name":"Dwarven",
+            "buffs":["DWARVEN_0", "DWARVEN_1", "DWARVEN_2", "DWARVEN_3", "DWARVEN_4", "DWARVEN_5"]
+        },
+		
+		"BC_RESIST": {
+			"id":"BC_RESIST",
+            "name":"Resist",
+            "buffs":["RESIST_0", "RESIST_1", "RESIST_2", "RESIST_3", "RESIST_4", "RESIST_5", "RESIST_6", "RESIST_7", "RESIST_8", "RESIST_9", "RESIST_10", "RESIST_11", "RESIST_12", "RESIST_13", "RESIST_14"]
+        },
+		
+		"BC_SPECIAL": {
+			"id":"BC_SPECIAL",
+            "name":"Special",
+            "buffs":["SPECIAL_0", "SPECIAL_1", "SPECIAL_2", "SPECIAL_3", "SPECIAL_4", "SPECIAL_5", "SPECIAL_6", "SPECIAL_7", "SPECIAL_8", "SPECIAL_9", "SPECIAL_10", "SPECIAL_11", "SPECIAL_12", "SPECIAL_13", "SPECIAL_14", "SPECIAL_15", "SPECIAL_16", "SPECIAL_17", "SPECIAL_18", "SPECIAL_19", "SPECIAL_20", "SPECIAL_21", "SPECIAL_22", "SPECIAL_23"]
+        },
+		
+		"BC_OVERLORD": {
+			"id":"BC_OVERLORD",
+            "name":"Overlord",
+            "buffs":["OVERLORD_0", "OVERLORD_1", "OVERLORD_2", "OVERLORD_3", "OVERLORD_4", "OVERLORD_5", "OVERLORD_6", "OVERLORD_7", "OVERLORD_8", "OVERLORD_9"]
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/main/resources/data/service/buffer/json/npcs/60001.json b/src/main/resources/data/service/buffer/json/npcs/60001.json
new file mode 100644
index 0000000000..d028b28a0c
--- /dev/null
+++ b/src/main/resources/data/service/buffer/json/npcs/60001.json
@@ -0,0 +1,11 @@
+{
+    "npcId":60001,
+    "directFirstTalk":true,
+    "dialogType":"NPC",
+    "htmlFolder":"npc",
+    "canHeal":true,
+    "canCancel":true,
+    
+    "presetBuffCategories":["BC_FIGHTER", "BC_MAGE"],
+    "buffCategories":["BC_BUFFS","BC_SONGS","BC_DANCES","BC_CHANTS","BC_DWARVEN","BC_RESIST","BC_SPECIAL","BC_OVERLORD"]
+}
\ No newline at end of file
diff --git a/src/main/resources/data/service/buffer/json/voiced.json b/src/main/resources/data/service/buffer/json/voiced.json
new file mode 100644
index 0000000000..afcd88ccb9
--- /dev/null
+++ b/src/main/resources/data/service/buffer/json/voiced.json
@@ -0,0 +1,9 @@
+{
+    "dialogType":"COMMUNITY",
+    "htmlFolder":"community",
+    "canHeal":true,
+    "canCancel":true,
+    
+    "presetBuffCategories":["BC_FIGHTER", "BC_MAGE"],
+    "buffCategories":["BC_BUFFS","BC_SONGS","BC_DANCES","BC_CHANTS","BC_DWARVEN","BC_RESIST","BC_SPECIAL","BC_OVERLORD"]
+}
\ No newline at end of file
diff --git a/src/main/resources/data/stats/npcs/custom/custom_bufferservice.xml b/src/main/resources/data/stats/npcs/custom/custom_bufferservice.xml
new file mode 100644
index 0000000000..65fcd9a078
--- /dev/null
+++ b/src/main/resources/data/stats/npcs/custom/custom_bufferservice.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<list xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../xsd/npcs.xsd">
+	<npc id="60001" displayId="31324" name="Phoenia" usingServerSideName="true" title="Buffer" usingServerSideTitle="true" type="L2Npc">
+		<collision>
+			<radius normal="8" />
+			<height normal="23" />
+		</collision>
+	</npc>
+</list>
\ No newline at end of file
diff --git a/src/main/resources/sql/custom/custom_buffer_service_1_ulists.sql b/src/main/resources/sql/custom/custom_buffer_service_1_ulists.sql
new file mode 100644
index 0000000000..60202f86f1
--- /dev/null
+++ b/src/main/resources/sql/custom/custom_buffer_service_1_ulists.sql
@@ -0,0 +1,8 @@
+CREATE TABLE `custom_buffer_service_ulists` (
+  `ulist_id` int(10) unsigned NOT NULL AUTO_INCREMENT,
+  `ulist_char_id` int(10) unsigned NOT NULL,
+  `ulist_name` varchar(255) NOT NULL,
+  PRIMARY KEY (`ulist_id`),
+  UNIQUE KEY `ulist_char_id` (`ulist_char_id`,`ulist_name`),
+  CONSTRAINT `custom_buffer_service_ulists_ibfk_1` FOREIGN KEY (`ulist_char_id`) REFERENCES `characters` (`charId`) ON DELETE CASCADE ON UPDATE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
\ No newline at end of file
diff --git a/src/main/resources/sql/custom/custom_buffer_service_2_ulist_buffs.sql b/src/main/resources/sql/custom/custom_buffer_service_2_ulist_buffs.sql
new file mode 100644
index 0000000000..03b298aa45
--- /dev/null
+++ b/src/main/resources/sql/custom/custom_buffer_service_2_ulist_buffs.sql
@@ -0,0 +1,6 @@
+CREATE TABLE `custom_buffer_service_ulist_buffs` (
+  `ulist_id` int(10) unsigned NOT NULL,
+  `ulist_buff_ident` varchar(255) NOT NULL,
+  PRIMARY KEY (`ulist_id`,`ulist_buff_ident`),
+  CONSTRAINT `custom_buffer_service_ulist_buffs_ibfk_1` FOREIGN KEY (`ulist_id`) REFERENCES `custom_buffer_service_ulists` (`ulist_id`) ON DELETE CASCADE ON UPDATE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
\ No newline at end of file
-- 
GitLab