9
0
mirror of https://github.com/Xiao-MoMi/Custom-Fishing.git synced 2025-12-29 03:49:07 +00:00

lock improvements

This commit is contained in:
XiaoMoMi
2023-09-07 03:59:27 +08:00
parent 20b4a73716
commit 32d309c192
21 changed files with 170 additions and 80 deletions

View File

@@ -19,14 +19,20 @@ package net.momirealms.customfishing.command.sub;
import dev.jorel.commandapi.CommandAPICommand;
import dev.jorel.commandapi.arguments.OfflinePlayerArgument;
import dev.jorel.commandapi.arguments.PlayerArgument;
import dev.jorel.commandapi.arguments.StringArgument;
import dev.jorel.commandapi.arguments.UUIDArgument;
import net.momirealms.customfishing.adventure.AdventureManagerImpl;
import net.momirealms.customfishing.api.CustomFishingPlugin;
import net.momirealms.customfishing.api.data.user.OfflineUser;
import net.momirealms.customfishing.setting.Locale;
import net.momirealms.customfishing.storage.user.OfflineUserImpl;
import org.bukkit.Bukkit;
import org.bukkit.OfflinePlayer;
import org.bukkit.entity.Player;
import org.bukkit.inventory.Inventory;
import java.util.Objects;
import java.util.UUID;
public class FishingBagCommand {
@@ -36,37 +42,59 @@ public class FishingBagCommand {
public CommandAPICommand getBagCommand() {
return new CommandAPICommand("fishingbag")
.withPermission("fishingbag.user")
.withSubcommand(getAdminCommand())
.withSubcommands(getEditOnlineCommand(), getEditOfflineCommand())
.executesPlayer(((player, args) -> {
var inv = CustomFishingPlugin.get().getBagManager().getOnlineBagInventory(player.getUniqueId());
if (inv != null) player.openInventory(inv);
if (inv != null) {
player.openInventory(inv);
} else {
AdventureManagerImpl.getInstance().sendMessageWithPrefix(player, Locale.MSG_Data_Not_Loaded);
}
}));
}
private CommandAPICommand getAdminCommand() {
return new CommandAPICommand("edit")
private CommandAPICommand getEditOnlineCommand() {
return new CommandAPICommand("edit-online")
.withPermission("fishingbag.admin")
.withArguments(new OfflinePlayerArgument("player"))
.withArguments(new PlayerArgument("player"))
.executesPlayer(((player, args) -> {
OfflinePlayer offlinePlayer = (OfflinePlayer) args.get("player");
UUID uuid = offlinePlayer.getUniqueId();
Player player1 = (Player) args.get("player");
UUID uuid = player1.getUniqueId();
Inventory onlineInv = CustomFishingPlugin.get().getBagManager().getOnlineBagInventory(uuid);
if (onlineInv != null) {
player.openInventory(onlineInv);
} else {
CustomFishingPlugin.get().getStorageManager().getOfflineUser(uuid, false).thenAccept(optional -> {
if (optional.isEmpty()) {
AdventureManagerImpl.getInstance().sendMessageWithPrefix(player, Locale.MSG_Unsafe_Modification);
} else {
OfflineUser offlineUser = optional.get();
if (offlineUser == OfflineUserImpl.NEVER_PLAYED_USER) {
AdventureManagerImpl.getInstance().sendMessageWithPrefix(player, Locale.MSG_Never_Played);
} else {
}
}
});
}
}));
}
private CommandAPICommand getEditOfflineCommand() {
return new CommandAPICommand("edit-offline")
.withPermission("fishingbag.admin")
.withArguments(new UUIDArgument("UUID"))
.executesPlayer(((player, args) -> {
UUID uuid = (UUID) args.get("UUID");
Player online = Bukkit.getPlayer(uuid);
if (online != null) {
Inventory onlineInv = CustomFishingPlugin.get().getBagManager().getOnlineBagInventory(uuid);
if (onlineInv != null) {
player.openInventory(onlineInv);
return;
}
}
CustomFishingPlugin.get().getStorageManager().getOfflineUser(uuid, false).thenAccept(optional -> {
if (optional.isEmpty()) {
AdventureManagerImpl.getInstance().sendMessageWithPrefix(player, Locale.MSG_Never_Played);
return;
}
OfflineUser offlineUser = optional.get();
if (offlineUser == OfflineUserImpl.LOCKED_USER) {
AdventureManagerImpl.getInstance().sendMessageWithPrefix(player, Locale.MSG_Unsafe_Modification);
return;
}
CustomFishingPlugin.get().getScheduler().runTaskSync(() -> {
CustomFishingPlugin.get().getBagManager().editOfflinePlayerBag(player, offlineUser);
}, player.getLocation());
});
}));
}
}

View File

@@ -268,7 +268,7 @@ public class ActionManagerImpl implements ActionManager {
int delay;
if (args instanceof ConfigurationSection section) {
delay = section.getInt("delay", 1);
ConfigurationSection actionSection = section.getConfigurationSection("action");
ConfigurationSection actionSection = section.getConfigurationSection("actions");
if (actionSection != null) {
for (Map.Entry<String, Object> entry : actionSection.getValues(false).entrySet()) {
if (entry.getValue() instanceof ConfigurationSection innerSection) {

View File

@@ -19,23 +19,31 @@ package net.momirealms.customfishing.mechanic.bag;
import net.momirealms.customfishing.CustomFishingPluginImpl;
import net.momirealms.customfishing.api.CustomFishingPlugin;
import net.momirealms.customfishing.api.data.user.OfflineUser;
import net.momirealms.customfishing.api.manager.BagManager;
import net.momirealms.customfishing.api.mechanic.bag.FishingBagHolder;
import net.momirealms.customfishing.api.util.LogUtils;
import net.momirealms.customfishing.setting.Config;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.HandlerList;
import org.bukkit.event.Listener;
import org.bukkit.event.inventory.InventoryCloseEvent;
import org.bukkit.event.player.PlayerQuitEvent;
import org.bukkit.inventory.Inventory;
import java.util.HashMap;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
public class BagManagerImpl implements BagManager {
public class BagManagerImpl implements BagManager, Listener {
private final CustomFishingPlugin plugin;
private final ConcurrentHashMap<UUID, FishingBagHolder> bagMap;
private final HashMap<UUID, OfflineUser> tempEditMap;
public BagManagerImpl(CustomFishingPluginImpl plugin) {
this.plugin = plugin;
this.bagMap = new ConcurrentHashMap<>();
this.tempEditMap = new HashMap<>();
}
@Override
@@ -44,11 +52,11 @@ public class BagManagerImpl implements BagManager {
}
public void load() {
Bukkit.getPluginManager().registerEvents(this, plugin);
}
public void unload() {
HandlerList.unregisterAll(this);
}
public void disable() {
@@ -59,9 +67,33 @@ public class BagManagerImpl implements BagManager {
public Inventory getOnlineBagInventory(UUID uuid) {
var onlinePlayer = plugin.getStorageManager().getOnlineUser(uuid);
if (onlinePlayer == null) {
LogUtils.warn("Player " + uuid + "'s bag data is not loaded.");
return null;
}
return onlinePlayer.getHolder().getInventory();
}
@Override
public void editOfflinePlayerBag(Player admin, OfflineUser userData) {
this.tempEditMap.put(admin.getUniqueId(), userData);
admin.openInventory(userData.getHolder().getInventory());
}
@EventHandler
public void onInvClose(InventoryCloseEvent event) {
if (!(event.getInventory().getHolder() instanceof FishingBagHolder))
return;
final Player viewer = (Player) event.getPlayer();
OfflineUser offlineUser = tempEditMap.remove(viewer.getUniqueId());
if (offlineUser == null)
return;
plugin.getStorageManager().saveUserData(offlineUser, true);
}
@EventHandler
public void onQuit(PlayerQuitEvent event) {
OfflineUser offlineUser = tempEditMap.remove(event.getPlayer().getUniqueId());
if (offlineUser == null)
return;
plugin.getStorageManager().saveUserData(offlineUser, true);
}
}

View File

@@ -468,7 +468,8 @@ public class FishingManagerImpl implements Listener, FishingManager {
if (loot.getID().equals("vanilla")) {
ItemStack itemStack = this.vanillaLootMap.remove(fishingPreparation.getPlayer().getUniqueId());
if (itemStack != null) {
fishingPreparation.insertArg("loot", "<lang:item.minecraft." + itemStack.getType().toString().toLowerCase() + ">");
fishingPreparation.insertArg("{nick}", "<lang:item.minecraft." + itemStack.getType().toString().toLowerCase() + ">");
fishingPreparation.insertArg("{loot}", itemStack.getType().toString());
}
}

View File

@@ -134,8 +134,8 @@ public class LootManagerImpl implements LootManager {
.showInFinder(section.getBoolean("show-in-fishfinder", showInFinder))
.gameConfig(section.getString("game-group", gameGroup))
.nick(section.getString("nick", section.getString("display.name", key)))
.addActions(getActionMap(section.getConfigurationSection("action")))
.addTimesActions(getTimesActionMap(section.getConfigurationSection("action.success-times")))
.addActions(getActionMap(section.getConfigurationSection("events")))
.addTimesActions(getTimesActionMap(section.getConfigurationSection("events.success-times")))
.build();
}

View File

@@ -55,6 +55,7 @@ public class Locale {
public static String FORMAT_Hour;
public static String FORMAT_Minute;
public static String FORMAT_Second;
public static String MSG_Data_Not_Loaded;
public static void load() {
try {
@@ -99,6 +100,7 @@ public class Locale {
MSG_Give_Item = msgSection.getString("give-item");
MSG_Never_Played = msgSection.getString("never-played");
MSG_Unsafe_Modification = msgSection.getString("unsafe-modification");
MSG_Data_Not_Loaded = msgSection.getString("data-not-loaded");
}
}
}

View File

@@ -113,7 +113,7 @@ public class StorageManagerImpl implements StorageManager, Listener {
this.timerSaveTask = this.plugin.getScheduler().runTaskAsyncTimer(
() -> {
long time1 = System.currentTimeMillis();
this.dataSource.setPlayersData(this.onlineUserMap.values(), false);
this.dataSource.saveOnlinePlayersData(this.onlineUserMap.values(), false);
LogUtils.info("Data Saved for online players. Took " + (System.currentTimeMillis() - time1) + "ms.");
},
Config.dataSaveInterval,
@@ -124,7 +124,7 @@ public class StorageManagerImpl implements StorageManager, Listener {
public void disable() {
HandlerList.unregisterAll(this);
this.dataSource.setPlayersData(onlineUserMap.values(), true);
this.dataSource.saveOnlinePlayersData(onlineUserMap.values(), true);
this.onlineUserMap.clear();
if (this.dataSource != null)
this.dataSource.disable();
@@ -151,15 +151,20 @@ public class StorageManagerImpl implements StorageManager, Listener {
return CompletableFuture.completedFuture(Optional.empty());
}
PlayerData data = optionalUser.get();
if (data == PlayerData.NEVER_PLAYED) {
return CompletableFuture.completedFuture(Optional.of(OfflineUserImpl.NEVER_PLAYED_USER));
if (data == PlayerData.LOCKED) {
return CompletableFuture.completedFuture(Optional.of(OfflineUserImpl.LOCKED_USER));
} else {
OfflineUserImpl offlineUser = new OfflineUserImpl(uuid, data.getName(), data);
OfflineUser offlineUser = new OfflineUserImpl(uuid, data.getName(), data);
return CompletableFuture.completedFuture(Optional.of(offlineUser));
}
});
}
@Override
public CompletableFuture<Boolean> saveUserData(OfflineUser offlineUser, boolean unlock) {
return dataSource.savePlayerData(offlineUser.getUUID(), offlineUser.getPlayerData(), unlock);
}
@Override
public CompletableFuture<Integer> getRedisPlayerCount() {
return redisManager.getPlayerCount();
@@ -195,13 +200,13 @@ public class StorageManagerImpl implements StorageManager, Listener {
if (hasRedis) {
redisManager.setChangeServer(uuid).thenRun(
() -> redisManager.setPlayerData(uuid, data, true).thenRun(
() -> dataSource.setPlayerData(uuid, data, true).thenAccept(
() -> redisManager.savePlayerData(uuid, data, true).thenRun(
() -> dataSource.savePlayerData(uuid, data, true).thenAccept(
result -> {
if (result) locked.remove(uuid);
})));
} else {
dataSource.setPlayerData(uuid, data, true).thenAccept(
dataSource.savePlayerData(uuid, data, true).thenAccept(
result -> {
if (result) locked.remove(uuid);
});
@@ -210,8 +215,8 @@ public class StorageManagerImpl implements StorageManager, Listener {
public void redisReadingData(UUID uuid) {
// delay 0.5s for another server to insert the key
plugin.getScheduler().runTaskAsyncLater(() -> redisManager.getChangeServer(uuid).thenAccept(result -> {
if (!result) {
plugin.getScheduler().runTaskAsyncLater(() -> redisManager.getChangeServer(uuid).thenAccept(changeServer -> {
if (!changeServer) {
waitForDataLockRelease(uuid, 3);
} else {
new RedisGetDataTask(uuid);
@@ -260,11 +265,14 @@ public class StorageManagerImpl implements StorageManager, Listener {
if (player == null || !player.isOnline() || times > 3)
return;
this.dataSource.getPlayerData(uuid, false).thenAccept(optionalData -> {
if (optionalData.isEmpty()) {
waitForDataLockRelease(uuid, times + 1);
} else {
putDataInCache(player, optionalData.get());
}
// should not be empty
if (optionalData.isEmpty())
return;
if (optionalData.get() == PlayerData.LOCKED) {
waitForDataLockRelease(uuid, times + 1);
} else {
putDataInCache(player, optionalData.get());
}
});
}, 1, TimeUnit.SECONDS);
}

View File

@@ -47,9 +47,9 @@ public abstract class AbstractStorage implements DataStorageInterface {
}
@Override
public void setPlayersData(Collection<OnlineUser> users, boolean unlock) {
public void saveOnlinePlayersData(Collection<OnlineUser> users, boolean unlock) {
for (OnlineUser user : users) {
this.setPlayerData(user.getUUID(), user.getPlayerData(), unlock);
this.savePlayerData(user.getUUID(), user.getPlayerData(), unlock);
}
}
}

View File

@@ -112,17 +112,17 @@ public class MongoDBImpl extends AbstractStorage {
public CompletableFuture<Optional<PlayerData>> getPlayerData(UUID uuid, boolean force) {
var future = new CompletableFuture<Optional<PlayerData>>();
plugin.getScheduler().runTaskAsync(() -> {
MongoCollection<Document> collection = database.getCollection("movies");
MongoCollection<Document> collection = database.getCollection(getCollectionName("data"));
Document doc = collection.find(Filters.eq("uuid", uuid)).first();
if (doc == null) {
if (Bukkit.getPlayer(uuid) != null) {
future.complete(Optional.of(PlayerData.empty()));
} else {
future.complete(Optional.of(PlayerData.NEVER_PLAYED));
future.complete(Optional.empty());
}
} else {
if (!force && doc.getInteger("lock") != 0) {
future.complete(Optional.empty());
future.complete(Optional.of(PlayerData.LOCKED));
return;
}
Binary binary = (Binary) doc.get("data");
@@ -133,7 +133,7 @@ public class MongoDBImpl extends AbstractStorage {
}
@Override
public CompletableFuture<Boolean> setPlayerData(UUID uuid, PlayerData playerData, boolean unlock) {
public CompletableFuture<Boolean> savePlayerData(UUID uuid, PlayerData playerData, boolean unlock) {
var future = new CompletableFuture<Boolean>();
plugin.getScheduler().runTaskAsync(() -> {
MongoCollection<Document> collection = database.getCollection(getCollectionName("data"));
@@ -152,7 +152,7 @@ public class MongoDBImpl extends AbstractStorage {
}
@Override
public void setPlayersData(Collection<OnlineUser> users, boolean unlock) {
public void saveOnlinePlayersData(Collection<OnlineUser> users, boolean unlock) {
MongoCollection<Document> collection = database.getCollection(getCollectionName("data"));
try {
collection.insertMany(users.stream().map(it -> new Document()

View File

@@ -256,7 +256,7 @@ public class RedisManager extends AbstractStorage {
}
@Override
public CompletableFuture<Boolean> setPlayerData(UUID uuid, PlayerData playerData, boolean ignore) {
public CompletableFuture<Boolean> savePlayerData(UUID uuid, PlayerData playerData, boolean ignore) {
var future = new CompletableFuture<Boolean>();
plugin.getScheduler().runTaskAsync(() -> {
try (Jedis jedis = jedisPool.getResource()) {

View File

@@ -91,22 +91,21 @@ public abstract class AbstractSQLDatabase extends AbstractStorage {
if (rs.next()) {
int lock = rs.getInt(2);
if (!force && (lock != 0 && getCurrentSeconds() - Config.dataSaveInterval <= lock)) {
statement.close();
rs.close();
connection.close();
future.complete(Optional.empty());
statement.close(); rs.close(); connection.close();
future.complete(Optional.of(PlayerData.LOCKED));
return;
}
final Blob blob = rs.getBlob("data");
final byte[] dataByteArray = blob.getBytes(1, (int) blob.length());
blob.free();
lockPlayerData(uuid);
future.complete(Optional.of(plugin.getStorageManager().fromBytes(dataByteArray)));
} else if (Bukkit.getPlayer(uuid) != null) {
var data = PlayerData.empty();
insertPlayerData(uuid, data);
future.complete(Optional.of(data));
} else {
future.complete(Optional.of(PlayerData.NEVER_PLAYED));
future.complete(Optional.empty());
}
} catch (SQLException e) {
LogUtils.warn("Failed to get " + uuid + "'s data.", e);
@@ -117,7 +116,7 @@ public abstract class AbstractSQLDatabase extends AbstractStorage {
}
@Override
public CompletableFuture<Boolean> setPlayerData(UUID uuid, PlayerData playerData, boolean unlock) {
public CompletableFuture<Boolean> savePlayerData(UUID uuid, PlayerData playerData, boolean unlock) {
var future = new CompletableFuture<Boolean>();
plugin.getScheduler().runTaskAsync(() -> {
try (
@@ -138,7 +137,7 @@ public abstract class AbstractSQLDatabase extends AbstractStorage {
}
@Override
public void setPlayersData(Collection<OnlineUser> users, boolean unlock) {
public void saveOnlinePlayersData(Collection<OnlineUser> users, boolean unlock) {
String sql = String.format(SqlConstants.SQL_UPDATE_BY_UUID, getTableName("data"));
try (Connection connection = getConnection()) {
connection.setAutoCommit(false);
@@ -174,9 +173,23 @@ public abstract class AbstractSQLDatabase extends AbstractStorage {
}
}
public void lockPlayerData(UUID uuid) {
try (
Connection connection = getConnection();
PreparedStatement statement = connection.prepareStatement(String.format(SqlConstants.SQL_LOCK_BY_UUID, getTableName("data")))
) {
statement.setInt(1, getCurrentSeconds());
statement.setString(2, uuid.toString());
statement.execute();
} catch (SQLException e) {
LogUtils.warn("Failed to lock " + uuid + "'s data.", e);
}
}
public static class SqlConstants {
public static final String SQL_SELECT_BY_UUID = "SELECT * FROM `%s` WHERE `uuid` = ?";
public static final String SQL_UPDATE_BY_UUID = "UPDATE `%s` SET `lock` = ?, `data` = ? WHERE `uuid` = ?";
public static final String SQL_LOCK_BY_UUID = "UPDATE `%s` SET `lock` = ? WHERE `uuid` = ?";
public static final String SQL_INSERT_DATA_BY_UUID = "INSERT INTO `%s`(`uuid`, `lock`, `data`) VALUES(?, ?, ?)";
}
}

View File

@@ -90,20 +90,19 @@ public class SQLiteImpl extends AbstractSQLDatabase {
if (rs.next()) {
int lock = rs.getInt(2);
if (!force && (lock != 0 && getCurrentSeconds() - Config.dataSaveInterval <= lock)) {
statement.close();
rs.close();
connection.close();
future.complete(Optional.empty());
statement.close(); rs.close(); connection.close();
future.complete(Optional.of(PlayerData.LOCKED));
return;
}
final byte[] dataByteArray = rs.getBytes("data");
lockPlayerData(uuid);
future.complete(Optional.of(plugin.getStorageManager().fromBytes(dataByteArray)));
} else if (Bukkit.getPlayer(uuid) != null) {
var data = PlayerData.empty();
insertPlayerData(uuid, data);
future.complete(Optional.of(data));
} else {
future.complete(Optional.of(PlayerData.NEVER_PLAYED));
future.complete(Optional.empty());
}
} catch (SQLException e) {
LogUtils.warn("Failed to get " + uuid + "'s data.", e);
@@ -114,7 +113,7 @@ public class SQLiteImpl extends AbstractSQLDatabase {
}
@Override
public CompletableFuture<Boolean> setPlayerData(UUID uuid, PlayerData playerData, boolean unlock) {
public CompletableFuture<Boolean> savePlayerData(UUID uuid, PlayerData playerData, boolean unlock) {
var future = new CompletableFuture<Boolean>();
plugin.getScheduler().runTaskAsync(() -> {
try (
@@ -135,7 +134,7 @@ public class SQLiteImpl extends AbstractSQLDatabase {
}
@Override
public void setPlayersData(Collection<OnlineUser> users, boolean unlock) {
public void saveOnlinePlayersData(Collection<OnlineUser> users, boolean unlock) {
String sql = String.format(SqlConstants.SQL_UPDATE_BY_UUID, getTableName("data"));
try (Connection connection = getConnection()) {
connection.setAutoCommit(false);

View File

@@ -55,13 +55,13 @@ public class JsonImpl extends AbstractStorage {
} else if (Bukkit.getPlayer(uuid) != null) {
playerData = PlayerData.empty();
} else {
playerData = PlayerData.NEVER_PLAYED;
playerData = null;
}
return CompletableFuture.completedFuture(Optional.of(playerData));
return CompletableFuture.completedFuture(Optional.ofNullable(playerData));
}
@Override
public CompletableFuture<Boolean> setPlayerData(UUID uuid, PlayerData playerData, boolean ignore) {
public CompletableFuture<Boolean> savePlayerData(UUID uuid, PlayerData playerData, boolean ignore) {
this.saveToJsonFile(playerData, getPlayerDataFile(uuid));
return CompletableFuture.completedFuture(true);
}

View File

@@ -56,9 +56,9 @@ public class YAMLImpl extends AbstractStorage {
File dataFile = getPlayerDataFile(uuid);
if (!dataFile.exists()) {
if (Bukkit.getPlayer(uuid) != null) {
return CompletableFuture.completedFuture(Optional.of(PlayerData.empty()));
return CompletableFuture.completedFuture(Optional.of(PlayerData.LOCKED));
} else {
return CompletableFuture.completedFuture(Optional.of(PlayerData.NEVER_PLAYED));
return CompletableFuture.completedFuture(Optional.of(PlayerData.empty()));
}
}
YamlConfiguration data = ConfigUtils.readData(dataFile);
@@ -72,7 +72,7 @@ public class YAMLImpl extends AbstractStorage {
}
@Override
public CompletableFuture<Boolean> setPlayerData(UUID uuid, PlayerData playerData, boolean ignore) {
public CompletableFuture<Boolean> savePlayerData(UUID uuid, PlayerData playerData, boolean ignore) {
YamlConfiguration data = new YamlConfiguration();
data.set("name", playerData.getName());
data.set("bag", playerData.getBagData().serialized);

View File

@@ -43,7 +43,7 @@ public class OfflineUserImpl implements OfflineUser {
private final FishingBagHolder holder;
private final EarningData earningData;
private final Statistics statistics;
public static OfflineUserImpl NEVER_PLAYED_USER = new OfflineUserImpl(UUID.randomUUID(), "", PlayerData.empty());
public static OfflineUserImpl LOCKED_USER = new OfflineUserImpl(UUID.randomUUID(), "", PlayerData.empty());
public OfflineUserImpl(UUID uuid, String name, PlayerData playerData) {
this.name = name;

View File

@@ -28,7 +28,7 @@ mechanics:
disable-game: false
instant-game: false
prevent-grabbing: false
action:
events:
success:
title_action:
type: random-title

View File

@@ -23,4 +23,5 @@ messages:
goal-total-size: 'Total size of the fish caught'
goal-total-score: 'Total score of the fish caught'
unsafe-modification: 'You can''t edit a player''s fishing bag when the player is playing on another server that connected to the database'
never-played: 'Player {player} has never played the server. You can''t edit a non existent player''s fishing bag.'
never-played: 'That player has never played the server. You can''t edit a non existent player''s fishing bag.'
data-not-loaded: '<red>Your data has not been loaded yet. Try rejoining the server. If the problem still occurs, contact the server administrator.'