Minecraft Paper 1.20.4插件:Killstreak与Scoreboard无法保存问题求助
Minecraft Paper 1.20.4插件问题修复:击杀Streak数据保存与计分板持久化
核心问题
player_data.txt生成后无玩家数据- 玩家退出重进后,Killstreak对应的计分板无法保留
问题根源
- 数据保存时机单一:仅在插件禁用时保存数据,玩家中途退出服务器时,其击杀streak不会被写入文件,导致文件为空或数据不全。
- 登录时未加载数据:插件启动时只加载当时在线玩家的数据,新登录的玩家不会触发数据加载,重进后自然恢复不了计分板。
- 用Player对象做Map键的缺陷:玩家离线后,Player对象会被服务器回收,重新登录后是新对象,原Map中的数据无法关联到新玩家。
修复步骤及代码修改
1. 新增玩家登录/退出事件监听
添加PlayerJoinEvent和PlayerQuitEvent处理方法,确保玩家登录时加载数据,退出时立即保存自身数据。
2. 改用UUID存储玩家数据
将原来的Map<Player, Integer>改为Map<UUID, Integer>,用玩家唯一UUID作为键,避免名称变更或对象失效导致的数据丢失。
3. 调整数据加载与保存逻辑
- 加载数据时,先把所有玩家的UUID和streak存入Map,玩家登录时直接从Map中读取。
- 保存数据时,写入UUID而非玩家名称,保证数据的唯一性和可追溯性。
修改后的完整代码
package me.dev.Boost; import org.bukkit.Bukkit; import org.bukkit.ChatColor; import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; import org.bukkit.event.Listener; import org.bukkit.event.entity.PlayerDeathEvent; import org.bukkit.event.player.PlayerJoinEvent; import org.bukkit.event.player.PlayerQuitEvent; import org.bukkit.plugin.java.JavaPlugin; import org.bukkit.potion.PotionEffect; import org.bukkit.potion.PotionEffectType; import org.bukkit.scoreboard.DisplaySlot; import org.bukkit.scoreboard.Objective; import org.bukkit.scoreboard.Score; import org.bukkit.scoreboard.Scoreboard; import java.io.File; import java.io.IOException; import java.nio.file.Files; import java.util.HashMap; import java.util.Map; import java.util.UUID; import java.util.logging.Level; import java.util.logging.Logger; public class BoostPlugin extends JavaPlugin implements Listener { private final Map<UUID, Integer> killstreaks = new HashMap<>(); private final Map<Player, Scoreboard> playerScoreboards = new HashMap<>(); private File dataFile; private final Logger logger = Logger.getLogger("BoostPlugin"); @Override public void onEnable() { Bukkit.getServer().getPluginManager().registerEvents(this, this); dataFile = new File(getDataFolder(), "player_data.txt"); if (!dataFile.exists()) { try { dataFile.getParentFile().mkdirs(); dataFile.createNewFile(); } catch (IOException e) { logger.log(Level.SEVERE, "Failed to create data file", e); } } loadPlayerData(); logger.info("BoostPlugin enabled successfully."); } @Override public void onDisable() { savePlayerData(); logger.info("BoostPlugin disabled successfully."); } @EventHandler public void onPlayerJoin(PlayerJoinEvent event) { Player player = event.getPlayer(); UUID uuid = player.getUniqueId(); int streak = killstreaks.getOrDefault(uuid, 0); if (streak > 0) { updatePlayerScoreboard(player, streak); updatePlayerDisplayName(player, streak); applyEffects(player, streak); } } @EventHandler public void onPlayerQuit(PlayerQuitEvent event) { Player player = event.getPlayer(); UUID uuid = player.getUniqueId(); if (killstreaks.containsKey(uuid)) { saveSinglePlayerData(uuid, killstreaks.get(uuid)); } playerScoreboards.remove(player); killstreaks.remove(uuid); } @EventHandler public void onPlayerDeath(PlayerDeathEvent event) { Player victim = event.getEntity(); Player killer = victim.getKiller(); if (killer != null) { handleKill(killer); } resetKillstreak(victim); } private void handleKill(Player killer) { UUID uuid = killer.getUniqueId(); int currentStreak = killstreaks.getOrDefault(uuid, 0); int newStreak = currentStreak + 1; killer.getActivePotionEffects().forEach(effect -> killer.removePotionEffect(effect.getType())); applyEffects(killer, newStreak); killer.sendMessage(ChatColor.GREEN + "Killstreak: " + newStreak); updatePlayerScoreboard(killer, newStreak); updatePlayerDisplayName(killer, newStreak); killstreaks.put(uuid, newStreak); } private void applyEffects(Player player, int streak) { if (streak == 1) { player.addPotionEffect(new PotionEffect(PotionEffectType.HERO_OF_THE_VILLAGE, Integer.MAX_VALUE, 0, true, false)); } else if (streak == 2) { player.addPotionEffect(new PotionEffect(PotionEffectType.INCREASE_DAMAGE, Integer.MAX_VALUE, 0, true, false)); } else if (streak == 3) { player.addPotionEffect(new PotionEffect(PotionEffectType.HEALTH_BOOST, Integer.MAX_VALUE, 0, true, false)); } else if (streak == 4) { player.addPotionEffect(new PotionEffect(PotionEffectType.FAST_DIGGING, Integer.MAX_VALUE, 0, true, false)); } else if (streak >= 5) { player.addPotionEffect(new PotionEffect(PotionEffectType.DOLPHINS_GRACE, Integer.MAX_VALUE, 0, true, false)); } } private void resetKillstreak(Player player) { UUID uuid = player.getUniqueId(); if (killstreaks.containsKey(uuid)) { killstreaks.remove(uuid); player.sendMessage(ChatColor.RED + "Your killstreak has been reset."); removePlayerScoreboard(player); updatePlayerDisplayName(player, 0); player.getActivePotionEffects().forEach(effect -> player.removePotionEffect(effect.getType())); } } private void updatePlayerScoreboard(Player player, int streak) { Scoreboard scoreboard = playerScoreboards.computeIfAbsent(player, p -> Bukkit.getScoreboardManager().getNewScoreboard()); Objective objective = scoreboard.getObjective("killstreak"); if (objective == null) { objective = scoreboard.registerNewObjective("killstreak", "dummy", ChatColor.GREEN + "Killstreak"); objective.setDisplaySlot(DisplaySlot.BELOW_NAME); } Score score = objective.getScore(player.getName()); score.setScore(Math.min(streak, 5)); player.setScoreboard(scoreboard); } private void removePlayerScoreboard(Player player) { player.setScoreboard(Bukkit.getScoreboardManager().getMainScoreboard()); playerScoreboards.remove(player); } private void updatePlayerDisplayName(Player player, int streak) { String displayName = player.getName() + ChatColor.RESET; if (streak > 0) { displayName += " " + ChatColor.GREEN + "KS:" + Math.min(streak, 5); } player.setPlayerListName(displayName); } private void loadPlayerData() { try { if (!dataFile.exists()) { logger.info("Player data file does not exist, creating new one."); return; } Files.lines(dataFile.toPath()).forEach(line -> { String[] parts = line.split(":"); if (parts.length == 2) { try { UUID uuid = UUID.fromString(parts[0]); int streak = Integer.parseInt(parts[1]); killstreaks.put(uuid, streak); } catch (IllegalArgumentException e) { logger.warning("Invalid data line: " + line); } } }); logger.info("Loaded " + killstreaks.size() + " player killstreak records."); } catch (IOException e) { logger.log(Level.SEVERE, "Error loading player data", e); } } private void savePlayerData() { StringBuilder data = new StringBuilder(); for (Map.Entry<UUID, Integer> entry : killstreaks.entrySet()) { data.append(entry.getKey()).append(":").append(entry.getValue()).append("\n"); } try { Files.write(dataFile.toPath(), data.toString().getBytes()); logger.info("Saved " + killstreaks.size() + " player killstreak records."); } catch (IOException e) { logger.log(Level.SEVERE, "Error saving player data", e); } } private void saveSinglePlayerData(UUID uuid, int streak) { try { StringBuilder existingData = new StringBuilder(); if (dataFile.exists()) { existingData.append(Files.readString(dataFile.toPath())); } String oldLine = uuid + ":" + killstreaks.getOrDefault(uuid, 0); int index = existingData.indexOf(oldLine); if (index != -1) { int endIndex = existingData.indexOf("\n", index); existingData.delete(index, endIndex != -1 ? endIndex + 1 : existingData.length()); } existingData.append(uuid).append(":").append(streak).append("\n"); Files.write(dataFile.toPath(), existingData.toString().getBytes()); } catch (IOException e) { logger.log(Level.SEVERE, "Error saving data for player " + uuid, e); } } }
修复说明
- 玩家退出时会立即保存自身的击杀streak数据,确保
player_data.txt能实时写入数据。 - 玩家登录时自动加载历史击杀streak,并恢复计分板和对应效果。
- 使用UUID作为数据标识,彻底解决玩家名称变更或对象失效导致的数据丢失问题。
- 新增效果清除逻辑,避免玩家重置streak后旧效果残留。
内容的提问来源于stack exchange,提问作者gamer boi
相关产品推荐
相关产品推荐

