Skip to content

Commit 83ae1b3

Browse files
遗漏
1 parent 44ee924 commit 83ae1b3

6 files changed

Lines changed: 128 additions & 121 deletions

File tree

src/main/java/ict/minesunshineone/peek/PeekPlugin.java

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -125,12 +125,12 @@ public void onDisable() {
125125
// applyRestoredState 失败时状态文件保留,下次登录时 onPlayerJoin 自动恢复
126126
}
127127
} else {
128-
// 传送失败:至少恢复游戏模式,确保玩家绝不会卡在旁观模式;
129-
// 同时保留状态文件,下次登录时由 onPlayerJoin 传送回原位置完成完整恢复
128+
// 传送失败:原地恢复完整状态(游戏模式/生命/饥饿/药水),确保玩家绝不会卡在旁观模式;
129+
// 保留状态文件,下次登录时由 onPlayerJoin 传送回原位置完成完整恢复
130130
try {
131-
player.setGameMode(data.getOriginalGameMode());
131+
stateHandler.getStateRestorer().applyStateInPlace(player, data);
132132
} catch (Throwable t) {
133-
getLogger().warning(String.format("关服时无法恢复玩家 %s 的游戏模式,将在重连时恢复: %s",
133+
getLogger().warning(String.format("关服时无法恢复玩家 %s 的状态,将在重连时恢复: %s",
134134
player.getName(), t.getMessage()));
135135
}
136136
}
@@ -234,6 +234,16 @@ public StatisticsManager getStatisticsManager() {
234234
return statisticsManager;
235235
}
236236

237+
/**
238+
* 调试模式下输出日志(仅当配置 debug=true 时)。
239+
* 各处理器共用此实现,避免重复的调试日志样板代码。
240+
*/
241+
public void logDebug(String message, Object... args) {
242+
if (getConfig().getBoolean("debug", false)) {
243+
getLogger().info(String.format(message, args));
244+
}
245+
}
246+
237247
/**
238248
* 重新加载配置和消息文件
239249
* 用于调试或运行时更新配置

src/main/java/ict/minesunshineone/peek/handler/BossBarHandler.java

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -211,8 +211,6 @@ public void cleanup() {
211211
}
212212

213213
private void logDebug(String message, Object... args) {
214-
if (plugin.getConfig().getBoolean("debug", false)) {
215-
plugin.getLogger().info(String.format(message, args));
216-
}
214+
plugin.logDebug(message, args);
217215
}
218216
}

src/main/java/ict/minesunshineone/peek/handler/PeekStateHandler.java

Lines changed: 24 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -210,12 +210,7 @@ public void endPeek(Player peeker, boolean shouldRestore) {
210210
stopRangeCheckerAndBossBar(peeker);
211211

212212
if (shouldRestore && peeker.isOnline()) {
213-
if (peeker.isDead()) {
214-
startRespawnWatcher(peeker, data);
215-
} else {
216-
// 玩家未死亡,直接恢复状态
217-
stateRestorer.restorePlayerState(peeker, data);
218-
}
213+
restoreOrWatch(peeker, data);
219214
}
220215

221216
// 发送消息
@@ -323,31 +318,44 @@ private void recoverEndedSession(Player peeker, PeekData data) {
323318
if (!peeker.isOnline()) {
324319
return;
325320
}
321+
// 玩家已开始新的观察会话:绝不能用旧会话数据干预,否则会把正在观察的玩家
322+
// 拽回旧会话的原始位置并破坏新会话。此时新会话自行负责玩家状态。
323+
if (activePeeks.containsKey(peeker.getUniqueId())) {
324+
return;
325+
}
326326
// 玩家已不在旁观模式:说明 endPeek 等路径已完成恢复,无需重复处理,
327327
// 避免双重恢复 / 重复传送。仅当玩家确实还卡在旁观模式时才介入。
328328
if (peeker.getGameMode() != GameMode.SPECTATOR) {
329329
return;
330330
}
331331
logDebug("Session for %s ended mid-start; recovering player state", peeker.getName());
332+
restoreOrWatch(peeker, data);
333+
}
334+
335+
/**
336+
* 根据玩家是否死亡选择恢复方式:死亡则等待重生后恢复(避免对尸体设置状态),否则立即恢复。
337+
*/
338+
private void restoreOrWatch(Player peeker, PeekData data) {
332339
if (peeker.isDead()) {
333-
// 死亡时交给重生监视器,重生后再恢复,避免对尸体设置状态
334340
startRespawnWatcher(peeker, data);
335341
} else {
336342
stateRestorer.restorePlayerState(peeker, data);
337343
}
338344
}
339345

340346
private void setSelfPeekGameMode(Player peeker) {
347+
// 捕获当前会话,延迟任务执行前校验其有效性,防止会话已结束仍切入旁观模式(卡旁观者)
348+
final PeekData data = activePeeks.get(peeker.getUniqueId());
349+
if (data == null) {
350+
return;
351+
}
341352
peeker.getScheduler().execute(plugin, () -> {
342353
// 这里如果玩家离线了调度器会自动退役
343-
/*
344-
* if (!peeker.isOnline()) {
345-
* plugin.getLogger().warning(String.format("玩家 %s 在设置自我观察模式时已离线",
346-
* peeker.getName()));
347-
* endPeek(peeker, false);
348-
* return;
349-
* }
350-
*/
354+
355+
// 会话已结束/正在退出:放弃进入旁观模式,避免卡旁观者
356+
if (sessionEnded(peeker, data)) {
357+
return;
358+
}
351359

352360
if (peeker.isDead()) {
353361
plugin.getSLF4JLogger().warn("玩家 {} 在设置自我观察模式时已死亡", peeker.getName());
@@ -443,9 +451,7 @@ private void playSound(Player player, String soundKey) {
443451
}
444452

445453
private void logDebug(String message, Object... args) {
446-
if (plugin.getConfig().getBoolean("debug", false)) {
447-
plugin.getLogger().info(String.format(message, args));
448-
}
454+
plugin.logDebug(message, args);
449455
}
450456

451457
// ==================== 公共查询方法 ====================

src/main/java/ict/minesunshineone/peek/handler/PlayerStateRestorer.java

Lines changed: 31 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,6 @@ public PlayerStateRestorer(PeekPlugin plugin) {
3131
* @param data PeekData 数据
3232
*/
3333
public void restorePlayerState(Player peeker, PeekData data) {
34-
final Runnable onFailed = () -> handleTeleportFailure(peeker, data);
35-
3634
// 原始位置所在世界可能已被卸载/删除,导致 teleportAsync 同步抛出 IllegalArgumentException。
3735
// 这种异常既不会进入 thenAccept/exceptionally,也不会触发离线回调,
3836
// 会让玩家永久卡在旁观模式。因此提前校验,必要时回退到重生点恢复。
@@ -44,15 +42,26 @@ public void restorePlayerState(Player peeker, PeekData data) {
4442
return;
4543
}
4644

45+
teleportThenRestore(peeker, originalLocation, data, () -> handleTeleportFailure(peeker, data));
46+
}
47+
48+
/**
49+
* 在实体调度器上把玩家异步传送到指定位置,成功后应用恢复状态;
50+
* 任何失败路径(teleportAsync 同步抛出、异步异常、返回 false、调度器退役)都会执行 onFailed。
51+
*
52+
* @param peeker 观察者
53+
* @param destination 目标位置(调用方需保证世界非空)
54+
* @param data PeekData 数据
55+
* @param onFailed 传送失败时的回退逻辑
56+
*/
57+
private void teleportThenRestore(Player peeker, Location destination, PeekData data, Runnable onFailed) {
4758
final boolean scheduled = peeker.getScheduler().execute(plugin, () -> {
4859
try {
49-
// 强制清理任何骑乘/附身状态,防止卡在旁观者模式
60+
// 强制清理骑乘/附身状态并清除动量,保证传送干净
5061
PlayerStateUtil.forceExitRidingState(peeker);
51-
52-
// 在传送前先清除动量
5362
peeker.setVelocity(ZERO_VECTOR);
5463

55-
peeker.teleportAsync(originalLocation, TeleportCause.PLUGIN).thenAccept(success -> {
64+
peeker.teleportAsync(destination, TeleportCause.PLUGIN).thenAccept(success -> {
5665
if (success) {
5766
// teleportAsync 回调线程不确定,使用实体调度器确保线程安全
5867
peeker.getScheduler().run(plugin,
@@ -100,33 +109,7 @@ public void handleTeleportFailure(Player peeker, PeekData data) {
100109
};
101110

102111
if (spawnLoc != null && spawnLoc.getWorld() != null) {
103-
final boolean scheduled = peeker.getScheduler().execute(plugin, () -> {
104-
try {
105-
peeker.teleportAsync(spawnLoc, TeleportCause.PLUGIN).thenAccept(spawnSuccess -> {
106-
if (spawnSuccess) {
107-
// teleportAsync 回调线程不确定,使用实体调度器确保线程安全
108-
peeker.getScheduler().run(plugin,
109-
scheduledTask -> applyRestoredState(peeker, data),
110-
() -> plugin.getLogger().warning(
111-
String.format("玩家 %s 在重生点传送恢复后离线", peeker.getName())));
112-
} else {
113-
onFailed.run();
114-
}
115-
}).exceptionally(ex -> {
116-
plugin.getLogger().warning(String.format("传送玩家 %s 到重生点时发生异常: %s", peeker.getName(), ex.getMessage()));
117-
onFailed.run();
118-
return null;
119-
});
120-
} catch (Throwable t) {
121-
// 即使重生点传送同步抛出,也要强制恢复游戏模式,绝不卡旁观者
122-
plugin.getLogger().warning(String.format("传送玩家 %s 到重生点时同步异常: %s", peeker.getName(), t.getMessage()));
123-
onFailed.run();
124-
}
125-
}, onFailed, 1L);
126-
127-
if (!scheduled) {
128-
onFailed.run();
129-
}
112+
teleportThenRestore(peeker, spawnLoc, data, onFailed);
130113
} else {
131114
onFailed.run();
132115
}
@@ -178,10 +161,22 @@ public void forceStateRestoreWithoutTeleport(Player peeker, PeekData data) {
178161
* @param data PeekData 数据
179162
*/
180163
public void applyRestoredState(Player peeker, PeekData data) {
181-
// 再次确保动量为0
182-
peeker.setVelocity(new Vector(0, 0, 0));
164+
applyStateInPlace(peeker, data);
183165

184-
// 再次强制清理骑乘/附身状态,确保万无一失
166+
// 游戏模式已恢复,现在安全地清除状态文件
167+
plugin.getStateManager().clearPlayerState(peeker);
168+
}
169+
170+
/**
171+
* 原地恢复玩家状态(不传送、不清除状态文件)。
172+
* 用于关服等无法传送但仍需确保玩家退出旁观模式的场景;保留状态文件以便重连时完成完整恢复。
173+
*
174+
* @param peeker 观察者
175+
* @param data PeekData 数据
176+
*/
177+
public void applyStateInPlace(Player peeker, PeekData data) {
178+
// 再次确保动量为0并清理骑乘/附身状态,确保万无一失
179+
peeker.setVelocity(ZERO_VECTOR);
185180
PlayerStateUtil.forceExitRidingState(peeker);
186181

187182
peeker.setGameMode(data.getOriginalGameMode());
@@ -193,9 +188,6 @@ public void applyRestoredState(Player peeker, PeekData data) {
193188
// 清除现有效果并应用保存的效果
194189
peeker.getActivePotionEffects().forEach(effect -> peeker.removePotionEffect(effect.getType()));
195190
data.getPotionEffects().forEach(effect -> peeker.addPotionEffect(effect));
196-
197-
// 游戏模式已恢复,现在安全地清除状态文件
198-
plugin.getStateManager().clearPlayerState(peeker);
199191
}
200192

201193
/**

src/main/java/ict/minesunshineone/peek/handler/RangeChecker.java

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -203,8 +203,6 @@ public void cleanup() {
203203
}
204204

205205
private void logDebug(String message, Object... args) {
206-
if (plugin.getConfig().getBoolean("debug", false)) {
207-
plugin.getLogger().info(String.format(message, args));
208-
}
206+
plugin.logDebug(message, args);
209207
}
210208
}

src/main/java/ict/minesunshineone/peek/manager/PrivacyManager.java

Lines changed: 57 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -78,35 +78,66 @@ public void togglePrivateMode(Player player) {
7878
}
7979

8080
/**
81-
* 取消玩家的所有待处理请求
81+
* 取消玩家的所有待处理请求(既包括以其为目标的,也包括其作为请求者发出的)
8282
*/
8383
private synchronized void cancelAllPendingRequests(Player player) {
8484
UUID playerUuid = player.getUniqueId();
85-
86-
// 取消作为目标的所有请求
87-
Map<UUID, ScheduledTask> targetRequests = pendingRequests.get(playerUuid);
88-
if (targetRequests != null) {
89-
for (Map.Entry<UUID, ScheduledTask> entry : targetRequests.entrySet()) {
90-
entry.getValue().cancel();
85+
cancelIncomingRequests(playerUuid, "request-cancel");
86+
cancelOutgoingRequests(playerUuid, "request-cancel");
87+
}
88+
89+
/**
90+
* 取消所有以指定玩家为目标(外层 key)的待处理请求,按需通知请求者。
91+
*
92+
* @param targetUuid 目标玩家 UUID
93+
* @param requesterMessageKey 通知请求者的消息键,为 null 则不通知
94+
*/
95+
private void cancelIncomingRequests(UUID targetUuid, String requesterMessageKey) {
96+
Map<UUID, ScheduledTask> targetRequests = pendingRequests.remove(targetUuid);
97+
if (targetRequests == null) {
98+
return;
99+
}
100+
for (Map.Entry<UUID, ScheduledTask> entry : targetRequests.entrySet()) {
101+
entry.getValue().cancel();
102+
if (requesterMessageKey != null) {
91103
Player requester = plugin.getServer().getPlayer(entry.getKey());
92104
if (requester != null && requester.isOnline()) {
93-
plugin.getMessages().send(requester, "request-cancel");
105+
plugin.getMessages().send(requester, requesterMessageKey);
94106
}
95107
}
96-
pendingRequests.remove(playerUuid);
97108
}
98-
99-
// 取消作为请求者的所有请求
100-
for (Map.Entry<UUID, Map<UUID, ScheduledTask>> entry : pendingRequests.entrySet()) {
101-
ScheduledTask task = entry.getValue().remove(playerUuid);
102-
if (task != null) {
103-
task.cancel();
109+
}
110+
111+
/**
112+
* 取消指定玩家作为请求者发出的所有待处理请求,按需通知目标。
113+
*
114+
* @param peekerUuid 请求者 UUID
115+
* @param targetMessageKey 通知目标的消息键,为 null 则不通知
116+
* @return 是否取消了任何请求
117+
*/
118+
private boolean cancelOutgoingRequests(UUID peekerUuid, String targetMessageKey) {
119+
boolean cancelledAny = false;
120+
Iterator<Map.Entry<UUID, Map<UUID, ScheduledTask>>> it = pendingRequests.entrySet().iterator();
121+
while (it.hasNext()) {
122+
Map.Entry<UUID, Map<UUID, ScheduledTask>> entry = it.next();
123+
Map<UUID, ScheduledTask> requests = entry.getValue();
124+
ScheduledTask task = requests.remove(peekerUuid);
125+
if (task == null) {
126+
continue;
127+
}
128+
task.cancel();
129+
cancelledAny = true;
130+
if (targetMessageKey != null) {
104131
Player target = plugin.getServer().getPlayer(entry.getKey());
105132
if (target != null && target.isOnline()) {
106-
plugin.getMessages().send(target, "request-cancel");
133+
plugin.getMessages().send(target, targetMessageKey);
107134
}
108135
}
136+
if (requests.isEmpty()) {
137+
it.remove();
138+
}
109139
}
140+
return cancelledAny;
110141
}
111142

112143
public void sendPeekRequest(Player peeker, Player target) {
@@ -259,27 +290,14 @@ private void playSound(Player player, String configPath) {
259290

260291
public synchronized void cancelAllRequests(Player player) {
261292
UUID playerUuid = player.getUniqueId();
262-
263-
// 取消作为目标的请求
264-
Map<UUID, ScheduledTask> targetRequests = pendingRequests.remove(playerUuid);
265-
if (targetRequests != null) {
266-
targetRequests.values().forEach(ScheduledTask::cancel);
267-
}
268-
269-
// 取消作为请求者的请求
270-
pendingRequests.values().forEach(requests -> {
271-
ScheduledTask task = requests.remove(playerUuid);
272-
if (task != null) {
273-
task.cancel();
274-
}
275-
});
276-
277-
// 清理空的映射
278-
pendingRequests.values().removeIf(Map::isEmpty);
279-
280-
// 清理该玩家相关的请求冷却记录,避免离线玩家的键长期堆积
281-
String uuidStr = playerUuid.toString();
282-
requestCooldowns.keySet().removeIf(key -> key.contains(uuidStr));
293+
// 玩家退出:取消其相关的所有请求,不发送通知(双方多半已离线)
294+
cancelIncomingRequests(playerUuid, null);
295+
cancelOutgoingRequests(playerUuid, null);
296+
297+
// 顺便清理所有已过期的请求冷却记录,避免无限增长。
298+
// 注意:只删除已过期的条目,不删除仍生效的冷却,否则玩家可通过重连绕过冷却。
299+
long now = System.currentTimeMillis();
300+
requestCooldowns.values().removeIf(ts -> now - ts >= cooldownDuration * 1000L);
283301
}
284302

285303
public synchronized void handleAccept(Player player) {
@@ -328,24 +346,9 @@ public synchronized void handleDeny(Player player) {
328346
* 直接在锁内操作,避免对外暴露内部可变映射导致的并发问题。
329347
*/
330348
public synchronized void cancelRequestsByPeekerOnDeath(Player peeker) {
331-
UUID peekerUuid = peeker.getUniqueId();
332-
Iterator<Map.Entry<UUID, Map<UUID, ScheduledTask>>> it = pendingRequests.entrySet().iterator();
333-
while (it.hasNext()) {
334-
Map.Entry<UUID, Map<UUID, ScheduledTask>> entry = it.next();
335-
Map<UUID, ScheduledTask> requests = entry.getValue();
336-
ScheduledTask task = requests.remove(peekerUuid);
337-
if (task == null) {
338-
continue;
339-
}
340-
task.cancel();
349+
// 只给死亡的请求者发送一次提示,即使其有多个待处理请求
350+
if (cancelOutgoingRequests(peeker.getUniqueId(), "request-cancelled-death-target")) {
341351
plugin.getMessages().send(peeker, "request-cancelled-death");
342-
Player target = plugin.getServer().getPlayer(entry.getKey());
343-
if (target != null && target.isOnline()) {
344-
plugin.getMessages().send(target, "request-cancelled-death-target");
345-
}
346-
if (requests.isEmpty()) {
347-
it.remove();
348-
}
349352
}
350353
}
351354
}

0 commit comments

Comments
 (0)