getDataKeys() {
- checkData();
- return Collections.unmodifiableSet(data.keySet());
+ public ASlimefunDataContainer(String key, ADataContainer other, String sfId) {
+ super(key, other);
+ this.sfId = sfId;
}
-
- @Nullable public String getData(String key) {
- checkData();
- return getCacheInternal(key);
- }
-
- @Nonnull
- public String getKey() {
- return key;
- }
-
- public abstract void setData(String key, String val);
}
diff --git a/src/main/java/com/xzavier0722/mc/plugin/slimefun4/storage/controller/BlockDataController.java b/src/main/java/com/xzavier0722/mc/plugin/slimefun4/storage/controller/BlockDataController.java
index c54320ad18..e097ae654e 100644
--- a/src/main/java/com/xzavier0722/mc/plugin/slimefun4/storage/controller/BlockDataController.java
+++ b/src/main/java/com/xzavier0722/mc/plugin/slimefun4/storage/controller/BlockDataController.java
@@ -1,5 +1,8 @@
package com.xzavier0722.mc.plugin.slimefun4.storage.controller;
+import city.norain.slimefun4.api.menu.UniversalMenu;
+import city.norain.slimefun4.api.menu.UniversalMenuPreset;
+import city.norain.slimefun4.utils.InventoryUtil;
import com.xzavier0722.mc.plugin.slimefun4.storage.adapter.IDataSourceAdapter;
import com.xzavier0722.mc.plugin.slimefun4.storage.callback.IAsyncReadCallback;
import com.xzavier0722.mc.plugin.slimefun4.storage.common.DataScope;
@@ -8,6 +11,8 @@
import com.xzavier0722.mc.plugin.slimefun4.storage.common.RecordKey;
import com.xzavier0722.mc.plugin.slimefun4.storage.common.RecordSet;
import com.xzavier0722.mc.plugin.slimefun4.storage.common.ScopeKey;
+import com.xzavier0722.mc.plugin.slimefun4.storage.controller.attributes.UniversalBlock;
+import com.xzavier0722.mc.plugin.slimefun4.storage.controller.attributes.UniversalDataTrait;
import com.xzavier0722.mc.plugin.slimefun4.storage.event.SlimefunChunkDataLoadEvent;
import com.xzavier0722.mc.plugin.slimefun4.storage.task.DelayedSavingLooperTask;
import com.xzavier0722.mc.plugin.slimefun4.storage.task.DelayedTask;
@@ -17,11 +22,14 @@
import io.github.bakedlibs.dough.collections.Pair;
import io.github.thebusybiscuit.slimefun4.api.items.SlimefunItem;
import io.github.thebusybiscuit.slimefun4.implementation.Slimefun;
+import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
+import java.util.Optional;
import java.util.Set;
+import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
@@ -38,26 +46,73 @@
import org.bukkit.plugin.Plugin;
import org.bukkit.scheduler.BukkitTask;
+/**
+ * 方块数据控制器
+ *
+ * 用于管理区块中的 Slimefun 方块数据
+ *
+ * {@link SlimefunBlockData}
+ * {@link SlimefunUniversalData}
+ *
+ * @author Xzavier0722
+ * @author NoRainCity
+ */
public class BlockDataController extends ADataController {
-
+ /**
+ * 延迟写数据任务队列
+ */
private final Map delayedWriteTasks;
+ /**
+ * 区块数据缓存
+ */
private final Map loadedChunk;
+ /**
+ * 通用数据缓存
+ */
+ private final Map loadedUniversalData;
+ /**
+ * 方块物品栏快照
+ */
private final Map>> invSnapshots;
+ /**
+ * 全局控制器加载数据锁
+ *
+ * {@link ScopedLock}
+ */
private final ScopedLock lock;
+ /**
+ * 延时加载模式标志
+ */
private boolean enableDelayedSaving = false;
+
private int delayedSecond = 0;
private BukkitTask looperTask;
+ /**
+ * 区块数据加载模式
+ * {@link ChunkDataLoadMode}
+ */
private ChunkDataLoadMode chunkDataLoadMode;
+ /**
+ * 初始化加载中标志
+ */
private boolean initLoading = false;
BlockDataController() {
super(DataType.BLOCK_STORAGE);
delayedWriteTasks = new HashMap<>();
loadedChunk = new ConcurrentHashMap<>();
+ loadedUniversalData = new ConcurrentHashMap<>();
invSnapshots = new ConcurrentHashMap<>();
lock = new ScopedLock();
}
+ /**
+ * 初始化数据控制器
+ *
+ * @param dataAdapter 使用的 {@link IDataSourceAdapter}
+ * @param maxReadThread 最大数据库读线程数
+ * @param maxWriteThread 最大数据库写线程数
+ */
@Override
public void init(IDataSourceAdapter> dataAdapter, int maxReadThread, int maxWriteThread) {
super.init(dataAdapter, maxReadThread, maxWriteThread);
@@ -65,13 +120,29 @@ public void init(IDataSourceAdapter> dataAdapter, int maxReadThread, int maxWr
initLoadData();
}
+ /**
+ * 初始化加载数据
+ */
private void initLoadData() {
switch (chunkDataLoadMode) {
case LOAD_WITH_CHUNK -> loadLoadedChunks();
case LOAD_ON_STARTUP -> loadLoadedWorlds();
}
+
+ Bukkit.getScheduler()
+ .runTaskLater(
+ Slimefun.instance(),
+ () -> {
+ initLoading = true;
+ loadUniversalRecord();
+ initLoading = false;
+ },
+ 1);
}
+ /**
+ * 加载所有服务器已加载的世界中的数据
+ */
private void loadLoadedWorlds() {
Bukkit.getScheduler()
.runTaskLater(
@@ -86,6 +157,9 @@ private void loadLoadedWorlds() {
1);
}
+ /**
+ * 加载所有服务器已加载的世界区块中的数据
+ */
private void loadLoadedChunks() {
Bukkit.getScheduler()
.runTaskLater(
@@ -102,10 +176,17 @@ private void loadLoadedChunks() {
1);
}
+ /**
+ * 初始化延时加载任务
+ *
+ * @param p 插件实例
+ * @param delayedSecond 首次执行延时
+ * @param forceSavePeriod 强制保存周期
+ */
public void initDelayedSaving(Plugin p, int delayedSecond, int forceSavePeriod) {
checkDestroy();
if (delayedSecond < 1 || forceSavePeriod < 1) {
- throw new IllegalArgumentException("Second must be greater than 0!");
+ throw new IllegalArgumentException("save period second must be greater than 0!");
}
enableDelayedSaving = true;
this.delayedSecond = delayedSecond;
@@ -137,20 +218,56 @@ public void setDelayedSavingEnable(boolean isEnable) {
}
/**
- * Creates a new slimefun block data at specific location
+ * 在指定位置新建方块
*
- * @param l slimefun block location {@link Location}
- * @param sfId slimefun block id {@link SlimefunItem#getId()}
- * @return {@link SlimefunBlockData}
+ * @param l Slimefun 方块位置 {@link Location}
+ * @param sfId Slimefun 物品 ID {@link SlimefunItem#getId()}
+ * @return 方块数据, 由于 {@link SlimefunItem} 的不同会返回两种数据中的一种
+ * {@link SlimefunBlockData}
+ * {@link SlimefunUniversalData}
*/
@Nonnull
- public SlimefunBlockData createBlock(Location l, String sfId) {
+ public ASlimefunDataContainer createBlock(Location l, String sfId) {
checkDestroy();
- var re = getChunkDataCache(l.getChunk(), true).createBlockData(l, sfId);
- if (Slimefun.getRegistry().getTickerBlocks().contains(sfId)) {
- Slimefun.getTickerTask().enableTicker(l);
+ var sfItem = SlimefunItem.getById(sfId);
+
+ if (sfItem instanceof UniversalBlock) {
+ var re = createUniversalBlockData(l, sfId);
+ if (Slimefun.getRegistry().getTickerBlocks().contains(sfId)) {
+ Slimefun.getTickerTask().enableTicker(l, re.getUUID());
+ }
+ return re;
+ } else {
+ var re = getChunkDataCache(l.getChunk(), true).createBlockData(l, sfId);
+ if (Slimefun.getRegistry().getTickerBlocks().contains(sfId)) {
+ Slimefun.getTickerTask().enableTicker(l);
+ }
+ return re;
}
- return re;
+ }
+
+ @Nonnull
+ @ParametersAreNonnullByDefault
+ public SlimefunUniversalBlockData createUniversalBlockData(Location l, String sfId) {
+ checkDestroy();
+
+ var uuid = UUID.randomUUID();
+ var uniData = new SlimefunUniversalBlockData(uuid, sfId, l);
+
+ uniData.setIsDataLoaded(true);
+
+ loadedUniversalData.put(uuid, uniData);
+
+ var preset = UniversalMenuPreset.getPreset(sfId);
+ if (preset != null) {
+ uniData.setMenu(new UniversalMenu(preset, uuid, l));
+ }
+
+ Slimefun.getDatabaseManager()
+ .getBlockDataController()
+ .saveUniversalData(uuid, sfId, Set.of(UniversalDataTrait.BLOCK, UniversalDataTrait.INVENTORY));
+
+ return uniData;
}
void saveNewBlock(Location l, String sfId) {
@@ -169,6 +286,27 @@ void saveNewBlock(Location l, String sfId) {
scheduleWriteTask(scopeKey, key, data, true);
}
+ /**
+ * Save certain universal data
+ *
+ * @param uuid universal data uuid
+ * @param sfId the item universal data represents
+ */
+ void saveUniversalData(UUID uuid, String sfId, Set traits) {
+ var key = new RecordKey(DataScope.UNIVERSAL_RECORD);
+
+ var data = new RecordSet();
+ data.put(FieldKey.UNIVERSAL_UUID, uuid.toString());
+ data.put(FieldKey.SLIMEFUN_ID, sfId);
+ data.put(
+ FieldKey.UNIVERSAL_TRAITS,
+ String.join(",", traits.stream().map(Enum::name).toList()));
+
+ var scopeKey = new UUIDKey(DataScope.NONE, uuid);
+ removeDelayedBlockDataUpdates(scopeKey); // Shouldn't have.. But for safe..
+ scheduleWriteTask(scopeKey, key, data, true);
+ }
+
/**
* Remove slimefun block data at specific location
*
@@ -179,6 +317,15 @@ public void removeBlock(Location l) {
var removed = getChunkDataCache(l.getChunk(), true).removeBlockData(l);
if (removed == null) {
+ getUniversalBlockDataFromCache(l)
+ .ifPresentOrElse(data -> removeUniversalBlockData(data.getUUID(), l), () -> {
+ if (Bukkit.isPrimaryThread()) {
+ Slimefun.getBlockDataService()
+ .getUniversalDataUUID(l.getBlock())
+ .ifPresent(uuid -> removeUniversalBlockData(uuid, l));
+ }
+ });
+
return;
}
@@ -196,6 +343,55 @@ public void removeBlock(Location l) {
}
}
+ public void removeBlockData(Location l) {
+ checkDestroy();
+
+ var removed = getChunkDataCache(l.getChunk(), true).removeBlockData(l);
+
+ if (removed == null || !removed.isDataLoaded()) {
+ return;
+ }
+
+ var menu = removed.getBlockMenu();
+ if (menu != null) {
+ InventoryUtil.closeInventory(menu.toInventory());
+ }
+
+ if (Slimefun.getRegistry().getTickerBlocks().contains(removed.getSfId())) {
+ Slimefun.getTickerTask().disableTicker(l);
+ }
+ }
+
+ public void removeUniversalBlockData(UUID uuid, Location lastPresent) {
+ checkDestroy();
+
+ var toRemove = loadedUniversalData.get(uuid);
+
+ if (toRemove == null) {
+ return;
+ }
+
+ if (!toRemove.isDataLoaded()) {
+ return;
+ }
+
+ if (toRemove instanceof SlimefunUniversalBlockData ubd) {
+ toRemove.setPendingRemove(true);
+ removeUniversalBlockDirectly(uuid);
+
+ var menu = ubd.getMenu();
+ if (menu != null) {
+ menu.lock();
+ }
+
+ if (Slimefun.getRegistry().getTickerBlocks().contains(toRemove.getSfId())) {
+ Slimefun.getTickerTask().disableTicker(lastPresent);
+ }
+
+ loadedUniversalData.remove(uuid);
+ }
+ }
+
void removeBlockDirectly(Location l) {
checkDestroy();
var scopeKey = new LocationKey(DataScope.NONE, l);
@@ -206,6 +402,16 @@ void removeBlockDirectly(Location l) {
scheduleDeleteTask(scopeKey, key, true);
}
+ void removeUniversalBlockDirectly(UUID uuid) {
+ checkDestroy();
+ var scopeKey = new UUIDKey(DataScope.NONE, uuid);
+ removeDelayedBlockDataUpdates(scopeKey);
+
+ var key = new RecordKey(DataScope.UNIVERSAL_RECORD);
+ key.addCondition(FieldKey.UNIVERSAL_UUID, uuid.toString());
+ scheduleDeleteTask(scopeKey, key, true);
+ }
+
/**
* Get slimefun block data at specific location
*
@@ -245,9 +451,9 @@ public SlimefunBlockData getBlockData(Location l) {
}
/**
- * Get slimefun block data at specific location async
+ * Get slimefun block data at specific location asynchronous
*
- * @param l slimefun block location {@link Location}
+ * @param l slimefun block location {@link Location}
* @param callback operation when block data fetched {@link IAsyncReadCallback}
*/
public void getBlockDataAsync(Location l, IAsyncReadCallback callback) {
@@ -264,6 +470,77 @@ public SlimefunBlockData getBlockDataFromCache(Location l) {
return getBlockDataFromCache(LocationUtils.getChunkKey(l.getChunk()), LocationUtils.getLocKey(l));
}
+ /**
+ * Get slimefun universal data
+ *
+ * @param uuid universal data uuid {@link UUID}
+ */
+ @Nullable public SlimefunUniversalBlockData getUniversalBlockData(@Nonnull UUID uuid) {
+ checkDestroy();
+
+ var key = new RecordKey(DataScope.UNIVERSAL_RECORD);
+ key.addCondition(FieldKey.UNIVERSAL_UUID, uuid.toString());
+ key.addField(FieldKey.SLIMEFUN_ID);
+
+ var result = getData(key);
+
+ if (result.isEmpty()) {
+ return null;
+ }
+
+ var newData = new SlimefunUniversalBlockData(uuid, result.get(0).get(FieldKey.SLIMEFUN_ID));
+
+ Arrays.stream(result.get(0).get(FieldKey.UNIVERSAL_TRAITS).split(",")).forEach(tname -> {
+ for (UniversalDataTrait trait : UniversalDataTrait.values()) {
+ if (trait.name().equals(tname)) {
+ newData.getTraits().add(trait);
+ }
+ }
+ });
+
+ return newData;
+ }
+
+ /**
+ * Get slimefun universal data asynchronous
+ *
+ * @param uuid universal data uuid {@link UUID}
+ * @param callback operation when block data fetched {@link IAsyncReadCallback}
+ */
+ public void getUniversalBlockData(@Nonnull UUID uuid, IAsyncReadCallback callback) {
+ scheduleReadTask(() -> invokeCallback(callback, getUniversalBlockData(uuid)));
+ }
+
+ /**
+ * Get slimefun universal data from cache
+ *
+ * @param uuid universal data uuid {@link UUID}
+ */
+ @Nullable public SlimefunUniversalBlockData getUniversalBlockDataFromCache(@Nonnull UUID uuid) {
+ checkDestroy();
+
+ var cache = loadedUniversalData.get(uuid);
+
+ return cache == null
+ ? getUniversalBlockData(uuid)
+ : (cache instanceof SlimefunUniversalBlockData ubd ? ubd : null);
+ }
+
+ /**
+ * Get slimefun universal data from cache by location
+ *
+ * @param l Slimefun block location {@link Location}
+ */
+ public Optional getUniversalBlockDataFromCache(@Nonnull Location l) {
+ checkDestroy();
+
+ return loadedUniversalData.values().stream()
+ .filter(uniData -> uniData instanceof SlimefunUniversalBlockData ubd
+ && ubd.getLastPresent().toLocation().equals(l))
+ .map(data -> (SlimefunUniversalBlockData) data)
+ .findFirst();
+ }
+
/**
* Move block data to specific location
*
@@ -363,6 +640,8 @@ public void loadChunk(Chunk chunk, boolean isNewChunk) {
loadChunkData(chunkData);
+ // 按区块加载方块数据
+
var key = new RecordKey(DataScope.BLOCK_RECORD);
key.addField(FieldKey.LOCATION);
key.addField(FieldKey.SLIMEFUN_ID);
@@ -384,6 +663,7 @@ public void loadChunk(Chunk chunk, boolean isNewChunk) {
scheduleReadTask(() -> loadBlockData(blockData));
}
});
+
Bukkit.getPluginManager().callEvent(new SlimefunChunkDataLoadEvent(chunkData));
}
@@ -407,6 +687,43 @@ public void loadWorld(World world) {
Level.INFO, "世界 {0} 数据加载完成, 耗时 {1}ms", new Object[] {worldName, (System.currentTimeMillis() - start)});
}
+ public void loadUniversalRecord() {
+ var uniKey = new RecordKey(DataScope.UNIVERSAL_RECORD);
+ uniKey.addField(FieldKey.UNIVERSAL_UUID);
+ uniKey.addField(FieldKey.SLIMEFUN_ID);
+ uniKey.addField(FieldKey.UNIVERSAL_TRAITS);
+
+ getData(uniKey).forEach(data -> {
+ var sfId = data.get(FieldKey.SLIMEFUN_ID);
+ var sfItem = SlimefunItem.getById(sfId);
+
+ if (sfItem == null) {
+ return;
+ }
+
+ var uuid = data.getUUID(FieldKey.UNIVERSAL_UUID);
+ var traitsData = data.get(FieldKey.UNIVERSAL_TRAITS);
+ var traits = new HashSet();
+
+ if (traitsData != null && !traitsData.isBlank()) {
+ for (String traitStr : traitsData.split(",")) {
+ try {
+ traits.add(UniversalDataTrait.valueOf(traitStr.toUpperCase()));
+ } catch (IllegalArgumentException ignored) {
+ }
+ }
+ }
+
+ var uniData = traits.contains(UniversalDataTrait.BLOCK)
+ ? new SlimefunUniversalBlockData(uuid, sfId)
+ : new SlimefunUniversalData(uuid, sfId);
+
+ traits.forEach(t -> uniData.getTraits().add(t));
+
+ scheduleReadTask(() -> loadUniversalData(uniData));
+ });
+ }
+
private void loadChunkData(SlimefunChunkData chunkData) {
if (chunkData.isDataLoaded()) {
return;
@@ -447,33 +764,45 @@ public void loadBlockData(SlimefunBlockData blockData) {
return;
}
- getData(key)
- .forEach(recordSet -> blockData.setCacheInternal(
- recordSet.get(FieldKey.DATA_KEY),
- DataUtils.blockDataDebase64(recordSet.get(FieldKey.DATA_VALUE)),
- false));
- blockData.setIsDataLoaded(true);
-
- var menuPreset = BlockMenuPreset.getPreset(blockData.getSfId());
- if (menuPreset != null) {
- var menuKey = new RecordKey(DataScope.BLOCK_INVENTORY);
- menuKey.addCondition(FieldKey.LOCATION, blockData.getKey());
- menuKey.addField(FieldKey.INVENTORY_SLOT);
- menuKey.addField(FieldKey.INVENTORY_ITEM);
-
- var inv = new ItemStack[54];
- getData(menuKey)
- .forEach(record -> inv[record.getInt(FieldKey.INVENTORY_SLOT)] =
- record.getItemStack(FieldKey.INVENTORY_ITEM));
- blockData.setBlockMenu(new BlockMenu(menuPreset, blockData.getLocation(), inv));
-
- var content = blockData.getMenuContents();
- if (content != null) {
- invSnapshots.put(blockData.getKey(), InvStorageUtils.getInvSnapshot(content));
+ var sfItem = SlimefunItem.getById(blockData.getSfId());
+ var universal = sfItem instanceof UniversalBlock;
+
+ var kvData = getData(key);
+
+ var menuKey = new RecordKey(DataScope.BLOCK_INVENTORY);
+ menuKey.addCondition(FieldKey.LOCATION, blockData.getKey());
+ menuKey.addField(FieldKey.INVENTORY_SLOT);
+ menuKey.addField(FieldKey.INVENTORY_ITEM);
+
+ var invData = getData(menuKey);
+
+ if (universal) {
+ migrateUniversalData(blockData.getLocation(), blockData.getSfId(), kvData, invData);
+ } else {
+ kvData.forEach(recordSet -> blockData.setCacheInternal(
+ recordSet.get(FieldKey.DATA_KEY),
+ DataUtils.blockDataDebase64(recordSet.get(FieldKey.DATA_VALUE)),
+ false));
+
+ blockData.setIsDataLoaded(true);
+
+ var menuPreset = BlockMenuPreset.getPreset(blockData.getSfId());
+
+ if (menuPreset != null) {
+ var inv = new ItemStack[54];
+
+ invData.forEach(record ->
+ inv[record.getInt(FieldKey.INVENTORY_SLOT)] = record.getItemStack(FieldKey.INVENTORY_ITEM));
+
+ blockData.setBlockMenu(new BlockMenu(menuPreset, blockData.getLocation(), inv));
+
+ var content = blockData.getMenuContents();
+ if (content != null) {
+ invSnapshots.put(blockData.getKey(), InvStorageUtils.getInvSnapshot(content));
+ }
}
}
- var sfItem = SlimefunItem.getById(blockData.getSfId());
if (sfItem != null && sfItem.isTicking()) {
Slimefun.getTickerTask().enableTicker(blockData.getLocation());
}
@@ -495,6 +824,89 @@ public void loadBlockDataAsync(
invokeCallback(callback, blockDataList);
}
+ @ParametersAreNonnullByDefault
+ public void loadUniversalData(SlimefunUniversalData uniData) {
+ if (uniData.isDataLoaded()) {
+ return;
+ }
+
+ var key = new RecordKey(DataScope.UNIVERSAL_DATA);
+ key.addCondition(FieldKey.UNIVERSAL_UUID, uniData.getKey());
+ key.addField(FieldKey.DATA_KEY);
+ key.addField(FieldKey.DATA_VALUE);
+
+ lock.lock(key);
+
+ try {
+ if (uniData.isDataLoaded()) {
+ return;
+ }
+
+ getData(key)
+ .forEach(recordSet -> uniData.setCacheInternal(
+ recordSet.get(FieldKey.DATA_KEY),
+ DataUtils.blockDataDebase64(recordSet.get(FieldKey.DATA_VALUE)),
+ false));
+
+ uniData.setIsDataLoaded(true);
+
+ loadedUniversalData.putIfAbsent(uniData.getUUID(), uniData);
+
+ if (uniData.hasTrait(UniversalDataTrait.INVENTORY)) {
+ var menuPreset = UniversalMenuPreset.getPreset(uniData.getSfId());
+ if (menuPreset != null) {
+ var menuKey = new RecordKey(DataScope.UNIVERSAL_INVENTORY);
+ menuKey.addCondition(FieldKey.UNIVERSAL_UUID, uniData.getKey());
+ menuKey.addField(FieldKey.INVENTORY_SLOT);
+ menuKey.addField(FieldKey.INVENTORY_ITEM);
+
+ var inv = new ItemStack[54];
+
+ getData(menuKey)
+ .forEach(recordSet -> inv[recordSet.getInt(FieldKey.INVENTORY_SLOT)] =
+ recordSet.getItemStack(FieldKey.INVENTORY_ITEM));
+
+ var location = uniData.hasTrait(UniversalDataTrait.BLOCK)
+ ? ((SlimefunUniversalBlockData) uniData)
+ .getLastPresent()
+ .toLocation()
+ : null;
+
+ uniData.setMenu(new UniversalMenu(menuPreset, uniData.getUUID(), location, inv));
+
+ var content = uniData.getMenuContents();
+ if (content != null) {
+ invSnapshots.put(uniData.getKey(), InvStorageUtils.getInvSnapshot(content));
+ }
+ }
+ }
+
+ if (uniData.hasTrait(UniversalDataTrait.BLOCK)) {
+ var sfItem = SlimefunItem.getById(uniData.getSfId());
+
+ if (sfItem != null && sfItem.isTicking()) {
+ Slimefun.getTickerTask()
+ .enableTicker(
+ ((SlimefunUniversalBlockData) uniData)
+ .getLastPresent()
+ .toLocation(),
+ uniData.getUUID());
+ }
+ }
+ } finally {
+ lock.unlock(key);
+ }
+ }
+
+ @ParametersAreNonnullByDefault
+ public void loadUniversalDataAsync(
+ SlimefunUniversalData uniData, IAsyncReadCallback callback) {
+ scheduleReadTask(() -> {
+ loadUniversalData(uniData);
+ invokeCallback(callback, uniData);
+ });
+ }
+
public SlimefunChunkData getChunkData(Chunk chunk) {
checkDestroy();
loadChunk(chunk, false);
@@ -520,6 +932,21 @@ public void saveAllBlockInventories() {
}));
}
+ public void saveAllUniversalInventories() {
+ var uniData = new HashSet<>(loadedUniversalData.values());
+ uniData.forEach(data -> {
+ if (data.isPendingRemove() || !data.isDataLoaded()) {
+ return;
+ }
+ var menu = data.getMenu();
+ if (menu == null || !menu.isDirty()) {
+ return;
+ }
+
+ saveUniversalInventory(data);
+ });
+ }
+
public void saveBlockInventory(SlimefunBlockData blockData) {
var newInv = blockData.getMenuContents();
List> lastSave;
@@ -591,6 +1018,29 @@ public void removeAllDataInWorldAsync(World world, Runnable onFinishedCallback)
});
}
+ public void saveUniversalInventory(@Nonnull SlimefunUniversalData universalData) {
+ var menu = universalData.getMenu();
+ var universalID = universalData.getUUID();
+
+ var newInv = menu.getContents();
+ List> lastSave;
+ if (newInv == null) {
+ lastSave = invSnapshots.remove(universalID.toString());
+ if (lastSave == null) {
+ return;
+ }
+ } else {
+ lastSave = invSnapshots.put(universalID.toString(), InvStorageUtils.getInvSnapshot(newInv));
+ }
+
+ var changed = InvStorageUtils.getChangedSlots(lastSave, newInv);
+ if (changed.isEmpty()) {
+ return;
+ }
+
+ changed.forEach(slot -> scheduleDelayedUniversalInvUpdate(universalID, menu, slot));
+ }
+
public Set getAllLoadedChunkData(World world) {
var prefix = world.getName() + ";";
var re = new HashSet();
@@ -648,9 +1098,47 @@ private void scheduleBlockInvUpdate(ScopeKey scopeKey, RecordKey reqKey, String
}
}
+ /**
+ * Save universal inventory by async way
+ *
+ * @param uuid Universal Inventory UUID
+ * @param menu Universal menu
+ * @param slot updated item slot
+ */
+ private void scheduleDelayedUniversalInvUpdate(UUID uuid, UniversalMenu menu, int slot) {
+ var scopeKey = new UUIDKey(DataScope.NONE, uuid);
+ var reqKey = new RecordKey(DataScope.UNIVERSAL_INVENTORY);
+ reqKey.addCondition(FieldKey.UNIVERSAL_UUID, uuid.toString());
+ reqKey.addCondition(FieldKey.INVENTORY_SLOT, slot + "");
+ reqKey.addField(FieldKey.INVENTORY_ITEM);
+
+ if (enableDelayedSaving) {
+ scheduleDelayedUpdateTask(
+ new LinkedKey(scopeKey, reqKey),
+ () -> scheduleUniversalInvUpdate(scopeKey, reqKey, uuid, menu.getContents(), slot));
+ } else {
+ scheduleUniversalInvUpdate(scopeKey, reqKey, uuid, menu.getContents(), slot);
+ }
+ }
+
+ private void scheduleUniversalInvUpdate(ScopeKey scopeKey, RecordKey reqKey, UUID uuid, ItemStack[] inv, int slot) {
+ var item = inv != null && slot < inv.length ? inv[slot] : null;
+
+ if (item == null) {
+ scheduleDeleteTask(scopeKey, reqKey, true);
+ } else {
+ var data = new RecordSet();
+ data.put(FieldKey.UNIVERSAL_UUID, uuid.toString());
+ data.put(FieldKey.INVENTORY_SLOT, slot + "");
+ data.put(FieldKey.INVENTORY_ITEM, item);
+ scheduleWriteTask(scopeKey, reqKey, data, true);
+ }
+ }
+
@Override
public void shutdown() {
saveAllBlockInventories();
+ saveAllUniversalInventories();
if (enableDelayedSaving) {
looperTask.cancel();
executeAllDelayedTasks();
@@ -672,6 +1160,21 @@ void scheduleDelayedBlockDataUpdate(SlimefunBlockData blockData, String key) {
}
}
+ void scheduleDelayedUniversalDataUpdate(SlimefunUniversalData universalData, String key) {
+ var scopeKey = new UUIDKey(DataScope.NONE, universalData.getKey());
+ var reqKey = new RecordKey(DataScope.UNIVERSAL_DATA);
+ reqKey.addCondition(FieldKey.UNIVERSAL_UUID, universalData.getKey());
+ reqKey.addCondition(FieldKey.DATA_KEY, key);
+ if (enableDelayedSaving) {
+ scheduleDelayedUpdateTask(
+ new LinkedKey(scopeKey, reqKey),
+ () -> scheduleUniversalDataUpdate(
+ scopeKey, reqKey, universalData.getKey(), key, universalData.getData(key)));
+ } else {
+ scheduleUniversalDataUpdate(scopeKey, reqKey, universalData.getKey(), key, universalData.getData(key));
+ }
+ }
+
private void removeDelayedBlockDataUpdates(ScopeKey scopeKey) {
synchronized (delayedWriteTasks) {
delayedWriteTasks
@@ -693,6 +1196,19 @@ private void scheduleBlockDataUpdate(ScopeKey scopeKey, RecordKey reqKey, String
}
}
+ private void scheduleUniversalDataUpdate(ScopeKey scopeKey, RecordKey reqKey, String uuid, String key, String val) {
+ if (val == null) {
+ scheduleDeleteTask(scopeKey, reqKey, false);
+ } else {
+ var data = new RecordSet();
+ reqKey.addField(FieldKey.DATA_VALUE);
+ data.put(FieldKey.UNIVERSAL_UUID, uuid);
+ data.put(FieldKey.DATA_KEY, key);
+ data.put(FieldKey.DATA_VALUE, DataUtils.blockDataBase64(val));
+ scheduleWriteTask(scopeKey, reqKey, data, true);
+ }
+ }
+
void scheduleDelayedChunkDataUpdate(SlimefunChunkData chunkData, String key) {
var scopeKey = new ChunkKey(DataScope.NONE, chunkData.getChunk());
var reqKey = new RecordKey(DataScope.CHUNK_DATA);
@@ -773,4 +1289,53 @@ private void clearBlockCacheAndTasks(SlimefunBlockData blockData) {
removeDelayedBlockDataUpdates(scopeKey);
abortScopeTask(scopeKey);
}
+
+ /**
+ * 迁移旧 Slimefun 机器数据至通用数据
+ */
+ private void migrateUniversalData(
+ @Nonnull Location l,
+ @Nonnull String sfId,
+ @Nonnull List kvData,
+ @Nonnull List invData) {
+ try {
+ if (l == null || sfId == null) {
+ return;
+ }
+
+ var universalData = createUniversalBlockData(l, sfId);
+
+ Slimefun.runSync(
+ () -> Slimefun.getBlockDataService()
+ .updateUniversalDataUUID(l.getBlock(), String.valueOf(universalData.getUUID())),
+ 10L);
+
+ kvData.forEach(recordSet -> universalData.setData(
+ recordSet.get(FieldKey.DATA_KEY), DataUtils.blockDataDebase64(recordSet.get(FieldKey.DATA_VALUE))));
+
+ var preset = UniversalMenuPreset.getPreset(sfId);
+ if (preset != null) {
+ final var inv = new ItemStack[54];
+
+ invData.forEach(record ->
+ inv[record.getInt(FieldKey.INVENTORY_SLOT)] = record.getItemStack(FieldKey.INVENTORY_ITEM));
+
+ universalData.setMenu(new UniversalMenu(preset, universalData.getUUID(), l, inv));
+
+ var content = universalData.getMenuContents();
+ if (content != null) {
+ invSnapshots.put(universalData.getKey(), InvStorageUtils.getInvSnapshot(content));
+ }
+ }
+
+ removeBlockData(l);
+
+ if (Slimefun.getRegistry().getTickerBlocks().contains(universalData.getSfId())) {
+ Slimefun.getTickerTask()
+ .enableTicker(universalData.getLastPresent().toLocation(), universalData.getUUID());
+ }
+ } catch (Exception e) {
+ Slimefun.logger().log(Level.WARNING, "迁移机器人数据时出现错误", e);
+ }
+ }
}
diff --git a/src/main/java/com/xzavier0722/mc/plugin/slimefun4/storage/controller/ControllerHolder.java b/src/main/java/com/xzavier0722/mc/plugin/slimefun4/storage/controller/ControllerHolder.java
index 763c9aabc1..544d4f6627 100644
--- a/src/main/java/com/xzavier0722/mc/plugin/slimefun4/storage/controller/ControllerHolder.java
+++ b/src/main/java/com/xzavier0722/mc/plugin/slimefun4/storage/controller/ControllerHolder.java
@@ -9,7 +9,8 @@ public class ControllerHolder {
private final Map controllers;
public static CT getController(Class clazz, StorageType type) {
- return ((ControllerHolder) holders.get(clazz)).get(type);
+ final var holder = holders.get(clazz);
+ return holder == null ? null : ((ControllerHolder) holder).get(type);
}
public static void clearControllers() {
diff --git a/src/main/java/com/xzavier0722/mc/plugin/slimefun4/storage/controller/SlimefunBlockData.java b/src/main/java/com/xzavier0722/mc/plugin/slimefun4/storage/controller/SlimefunBlockData.java
index 833a17e69c..417f669903 100644
--- a/src/main/java/com/xzavier0722/mc/plugin/slimefun4/storage/controller/SlimefunBlockData.java
+++ b/src/main/java/com/xzavier0722/mc/plugin/slimefun4/storage/controller/SlimefunBlockData.java
@@ -12,22 +12,18 @@
public class SlimefunBlockData extends ASlimefunDataContainer {
private final Location location;
- private final String sfId;
private volatile BlockMenu menu;
- private volatile boolean pendingRemove = false;
@ParametersAreNonnullByDefault
SlimefunBlockData(Location location, String sfId) {
- super(LocationUtils.getLocKey(location));
+ super(LocationUtils.getLocKey(location), sfId);
this.location = location;
- this.sfId = sfId;
}
@ParametersAreNonnullByDefault
SlimefunBlockData(Location location, SlimefunBlockData other) {
- super(LocationUtils.getLocKey(location), other);
+ super(LocationUtils.getLocKey(location), other, other.getSfId());
this.location = location;
- this.sfId = other.sfId;
}
@Nonnull
@@ -37,7 +33,7 @@ public Location getLocation() {
@Nonnull
public String getSfId() {
- return sfId;
+ return super.getSfId();
}
@ParametersAreNonnullByDefault
@@ -80,22 +76,14 @@ void setBlockMenu(BlockMenu blockMenu) {
return re;
}
- public void setPendingRemove(boolean pendingRemove) {
- this.pendingRemove = pendingRemove;
- }
-
- public boolean isPendingRemove() {
- return pendingRemove;
- }
-
@Override
public String toString() {
return "SlimefunBlockData [sfId="
- + sfId
+ + getSfId()
+ ", location="
+ location
+ ", isPendingRemove="
- + pendingRemove
+ + isPendingRemove()
+ "]";
}
}
diff --git a/src/main/java/com/xzavier0722/mc/plugin/slimefun4/storage/controller/SlimefunChunkData.java b/src/main/java/com/xzavier0722/mc/plugin/slimefun4/storage/controller/SlimefunChunkData.java
index 9dadc5afd0..174b150a8f 100644
--- a/src/main/java/com/xzavier0722/mc/plugin/slimefun4/storage/controller/SlimefunChunkData.java
+++ b/src/main/java/com/xzavier0722/mc/plugin/slimefun4/storage/controller/SlimefunChunkData.java
@@ -15,7 +15,7 @@
import org.bukkit.Chunk;
import org.bukkit.Location;
-public class SlimefunChunkData extends ASlimefunDataContainer {
+public class SlimefunChunkData extends ADataContainer {
private static final SlimefunBlockData INVALID_BLOCK_DATA = new SlimefunBlockData(
new Location(Bukkit.getWorlds().get(0), Double.MAX_VALUE, Double.MAX_VALUE, Double.MAX_VALUE),
"INVALID_BLOCK_DATA_SF_KEY");
diff --git a/src/main/java/com/xzavier0722/mc/plugin/slimefun4/storage/controller/SlimefunUniversalBlockData.java b/src/main/java/com/xzavier0722/mc/plugin/slimefun4/storage/controller/SlimefunUniversalBlockData.java
new file mode 100644
index 0000000000..c1b4355dd2
--- /dev/null
+++ b/src/main/java/com/xzavier0722/mc/plugin/slimefun4/storage/controller/SlimefunUniversalBlockData.java
@@ -0,0 +1,46 @@
+package com.xzavier0722.mc.plugin.slimefun4.storage.controller;
+
+import com.xzavier0722.mc.plugin.slimefun4.storage.controller.attributes.UniversalDataTrait;
+import com.xzavier0722.mc.plugin.slimefun4.storage.util.LocationUtils;
+import io.github.bakedlibs.dough.blocks.BlockPosition;
+import java.util.UUID;
+import org.bukkit.Location;
+
+public class SlimefunUniversalBlockData extends SlimefunUniversalData {
+ private volatile BlockPosition lastPresent;
+
+ public SlimefunUniversalBlockData(UUID uuid, String sfId) {
+ super(uuid, sfId);
+ }
+
+ public SlimefunUniversalBlockData(UUID uuid, String sfId, BlockPosition present) {
+ super(uuid, sfId);
+
+ this.lastPresent = present;
+ }
+
+ public SlimefunUniversalBlockData(UUID uuid, String sfId, Location present) {
+ this(uuid, sfId, new BlockPosition(present));
+ }
+
+ public void setLastPresent(BlockPosition lastPresent) {
+ setTraitData(UniversalDataTrait.BLOCK, LocationUtils.locationToString(lastPresent.toLocation()));
+ this.lastPresent = lastPresent;
+ }
+
+ public BlockPosition getLastPresent() {
+ if (lastPresent != null) {
+ return lastPresent;
+ }
+
+ var data = getData("location");
+
+ if (data == null) {
+ throw new IllegalArgumentException("UniversalBlockData missing location data");
+ }
+
+ lastPresent = new BlockPosition(LocationUtils.toLocation(data));
+
+ return lastPresent;
+ }
+}
diff --git a/src/main/java/com/xzavier0722/mc/plugin/slimefun4/storage/controller/SlimefunUniversalData.java b/src/main/java/com/xzavier0722/mc/plugin/slimefun4/storage/controller/SlimefunUniversalData.java
new file mode 100644
index 0000000000..baf6d59829
--- /dev/null
+++ b/src/main/java/com/xzavier0722/mc/plugin/slimefun4/storage/controller/SlimefunUniversalData.java
@@ -0,0 +1,93 @@
+package com.xzavier0722.mc.plugin.slimefun4.storage.controller;
+
+import city.norain.slimefun4.api.menu.UniversalMenu;
+import com.xzavier0722.mc.plugin.slimefun4.storage.controller.attributes.UniversalDataTrait;
+import io.github.thebusybiscuit.slimefun4.implementation.Slimefun;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.UUID;
+import java.util.logging.Level;
+import javax.annotation.Nullable;
+import javax.annotation.ParametersAreNonnullByDefault;
+import lombok.Getter;
+import lombok.Setter;
+import org.bukkit.inventory.ItemStack;
+
+@Getter
+public class SlimefunUniversalData extends ASlimefunDataContainer {
+ @Setter
+ private volatile UniversalMenu menu;
+
+ @Setter
+ private volatile boolean pendingRemove = false;
+
+ private final Set traits = new HashSet<>();
+
+ @ParametersAreNonnullByDefault
+ SlimefunUniversalData(UUID uuid, String sfId) {
+ super(uuid.toString(), sfId);
+ }
+
+ @ParametersAreNonnullByDefault
+ public void setData(String key, String val) {
+ checkData();
+
+ if (UniversalDataTrait.isReservedKey(key)) {
+ Slimefun.logger().log(Level.WARNING, "警告: 有附属正在尝试修改受保护的方块数据, 已取消更改");
+ return;
+ }
+
+ setCacheInternal(key, val, true);
+ Slimefun.getDatabaseManager().getBlockDataController().scheduleDelayedUniversalDataUpdate(this, key);
+ }
+
+ @ParametersAreNonnullByDefault
+ protected void setTraitData(UniversalDataTrait trait, String val) {
+ checkData();
+
+ if (!trait.getReservedKey().isBlank()) {
+ setCacheInternal(trait.getReservedKey(), val, true);
+ Slimefun.getDatabaseManager()
+ .getBlockDataController()
+ .scheduleDelayedUniversalDataUpdate(this, trait.getReservedKey());
+ }
+ }
+
+ @ParametersAreNonnullByDefault
+ public void removeData(String key) {
+ if (removeCacheInternal(key) != null || !isDataLoaded()) {
+ Slimefun.getDatabaseManager().getBlockDataController().scheduleDelayedUniversalDataUpdate(this, key);
+ }
+ }
+
+ @Nullable public ItemStack[] getMenuContents() {
+ if (menu == null) {
+ return null;
+ }
+ var re = new ItemStack[54];
+ var presetSlots = menu.getPreset().getPresetSlots();
+ var inv = menu.toInventory().getContents();
+ for (var i = 0; i < inv.length; i++) {
+ if (presetSlots.contains(i)) {
+ continue;
+ }
+ re[i] = inv[i];
+ }
+
+ return re;
+ }
+
+ public UUID getUUID() {
+ return UUID.fromString(getKey());
+ }
+
+ public boolean hasTrait(UniversalDataTrait trait) {
+ return traits.contains(trait);
+ }
+
+ @Override
+ public String toString() {
+ return "SlimefunUniversalData [uuid= " + getUUID() + ", sfId=" + getSfId() + ", isPendingRemove="
+ + pendingRemove + "]";
+ }
+}
diff --git a/src/main/java/com/xzavier0722/mc/plugin/slimefun4/storage/controller/StorageType.java b/src/main/java/com/xzavier0722/mc/plugin/slimefun4/storage/controller/StorageType.java
index 17852373b3..3e8579ecee 100644
--- a/src/main/java/com/xzavier0722/mc/plugin/slimefun4/storage/controller/StorageType.java
+++ b/src/main/java/com/xzavier0722/mc/plugin/slimefun4/storage/controller/StorageType.java
@@ -3,6 +3,5 @@
public enum StorageType {
MYSQL,
SQLITE,
-
POSTGRESQL,
}
diff --git a/src/main/java/com/xzavier0722/mc/plugin/slimefun4/storage/controller/attributes/UniversalBlock.java b/src/main/java/com/xzavier0722/mc/plugin/slimefun4/storage/controller/attributes/UniversalBlock.java
new file mode 100644
index 0000000000..0717588fc4
--- /dev/null
+++ b/src/main/java/com/xzavier0722/mc/plugin/slimefun4/storage/controller/attributes/UniversalBlock.java
@@ -0,0 +1,22 @@
+package com.xzavier0722.mc.plugin.slimefun4.storage.controller.attributes;
+
+import com.xzavier0722.mc.plugin.slimefun4.storage.controller.SlimefunUniversalBlockData;
+import com.xzavier0722.mc.plugin.slimefun4.storage.controller.SlimefunUniversalData;
+import io.github.thebusybiscuit.slimefun4.api.items.SlimefunItem;
+
+/**
+ * 这个属性用于声明 {@link SlimefunItem} 使用了 {@link SlimefunUniversalData}
+ *
+ * 当这个 {@link SlimefunItem} 作为机器时, 对应材质需要支持
+ * 使用 PDC 存储容器 (用于识别 UUID).
+ * 否则无法将这个物品/机器绑定到一个通用数据上.
+ *
+ * 查看此处了解支持 PDC 的物品材质:
+ * Paper Doc
+ *
+ * @author NoRainCity
+ *
+ * @see SlimefunUniversalData
+ * @see SlimefunUniversalBlockData
+ */
+public interface UniversalBlock {}
diff --git a/src/main/java/com/xzavier0722/mc/plugin/slimefun4/storage/controller/attributes/UniversalDataTrait.java b/src/main/java/com/xzavier0722/mc/plugin/slimefun4/storage/controller/attributes/UniversalDataTrait.java
new file mode 100644
index 0000000000..4d402a4118
--- /dev/null
+++ b/src/main/java/com/xzavier0722/mc/plugin/slimefun4/storage/controller/attributes/UniversalDataTrait.java
@@ -0,0 +1,44 @@
+package com.xzavier0722.mc.plugin.slimefun4.storage.controller.attributes;
+
+import city.norain.slimefun4.api.menu.UniversalMenu;
+import com.xzavier0722.mc.plugin.slimefun4.storage.controller.SlimefunUniversalBlockData;
+import com.xzavier0722.mc.plugin.slimefun4.storage.controller.SlimefunUniversalData;
+import lombok.Getter;
+
+/**
+ * 这个枚举类用于声明 {@link SlimefunUniversalData} 的特征.
+ * 一个通用数据可以有单个或多个特征.
+ *
+ * 对于一个通用数据, 它默认拥有作为 k-v 容器的特征.
+ *
+ * @see SlimefunUniversalData
+ * @see SlimefunUniversalBlockData
+ */
+@Getter
+public enum UniversalDataTrait {
+ /**
+ * BLOCK 特征标明该通用数据属于 {@link SlimefunUniversalBlockData}
+ */
+ BLOCK("location"),
+
+ /**
+ * INVENTORY 特征标明该通用数据拥有一个 {@link UniversalMenu}
+ */
+ INVENTORY("");
+
+ private final String reservedKey;
+
+ UniversalDataTrait(String reservedKey) {
+ this.reservedKey = reservedKey;
+ }
+
+ public static boolean isReservedKey(String key) {
+ for (UniversalDataTrait trait : UniversalDataTrait.values()) {
+ if (trait.getReservedKey().equals(key)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+}
diff --git a/src/main/java/com/xzavier0722/mc/plugin/slimefun4/storage/migrator/BlockStorageMigrator.java b/src/main/java/com/xzavier0722/mc/plugin/slimefun4/storage/migrator/BlockStorageMigrator.java
index 0700d6b20f..79f19083e5 100644
--- a/src/main/java/com/xzavier0722/mc/plugin/slimefun4/storage/migrator/BlockStorageMigrator.java
+++ b/src/main/java/com/xzavier0722/mc/plugin/slimefun4/storage/migrator/BlockStorageMigrator.java
@@ -2,6 +2,8 @@
import com.google.common.reflect.TypeToken;
import com.google.gson.Gson;
+import com.xzavier0722.mc.plugin.slimefun4.storage.controller.SlimefunBlockData;
+import com.xzavier0722.mc.plugin.slimefun4.storage.controller.SlimefunUniversalData;
import io.github.bakedlibs.dough.config.Config;
import io.github.thebusybiscuit.slimefun4.api.items.SlimefunItem;
import io.github.thebusybiscuit.slimefun4.implementation.Slimefun;
@@ -12,7 +14,7 @@
import java.util.Map;
import java.util.logging.Level;
import lombok.Getter;
-import me.mrCookieSlime.Slimefun.api.inventory.BlockMenu;
+import me.mrCookieSlime.Slimefun.api.inventory.DirtyChestMenu;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.World;
@@ -135,18 +137,24 @@ private void migrateBlock(World world, String sfId, String locStr, String jsonSt
var z = Integer.parseInt(arr[3]);
var loc = new Location(world, x, y, z);
- var blockData =
- Slimefun.getDatabaseManager().getBlockDataController().createBlock(loc, sfId);
+ var sfData = Slimefun.getDatabaseManager().getBlockDataController().createBlock(loc, sfId);
Map data = gson.fromJson(jsonStr, new TypeToken