Merge pull request 'fix(sanctuary): resynchroniser le panneau de quete apres rotation hors chunk' (#172) from fix/issue-167-quest-board-stale-sign into main
Verification 26.2 / check (push) Canceled after 0s

Reviewed-on: #172
This commit was merged in pull request #172.
This commit is contained in:
2026-09-09 13:25:24 +00:00
4 changed files with 72 additions and 13 deletions
+3 -1
View File
@@ -252,7 +252,9 @@ tasks.register("verifyBlackMarket") {
|| !quests.contains("SanctuaryRegistries.questBoardTier")
|| quests.contains("activationTier(")
|| quests.contains("QuestBoardTier.ACTIVATION_COST")
|| !quests.contains("addTreasury(claim.depositCurrency(), claim.depositAmount())")) {
|| !quests.contains("addTreasury(claim.depositCurrency(), claim.depositAmount())")
|| !quests.contains("shouldRewriteSign")
|| !quests.contains("syncLoadedSigns")) {
throw new GradleException("The physical Black Market escrow/delivery contract is incomplete")
}
}
@@ -75,9 +75,12 @@ final class QuestBoardGenerator {
private QuestBoardGenerator() {}
static QuestBoardData.Contract generate(MinecraftServer server, String boardId, QuestBoardTier tier, long now) {
long day = Math.floorDiv(now, 86_400_000L);
long seed = boardId.hashCode() * 0x9E3779B97F4A7C15L ^ day * 0xD1B54A32D192ED03L ^ tier.ordinal();
RandomSource random = RandomSource.create(seed);
return generate(server, boardId, tier, now, Math.floorDiv(now, 86_400_000L));
}
static QuestBoardData.Contract generate(MinecraftServer server, String boardId, QuestBoardTier tier,
long now, long salt) {
RandomSource random = RandomSource.create(contractSeed(boardId, tier, salt));
List<Template> pool = switch (tier) {
case EMERALD -> EASY;
case RUBY -> MEDIUM;
@@ -93,6 +96,10 @@ final class QuestBoardGenerator {
java.util.Optional.empty(), now);
}
static long contractSeed(String boardId, QuestBoardTier tier, long salt) {
return boardId.hashCode() * 0x9E3779B97F4A7C15L ^ salt * 0xD1B54A32D192ED03L ^ tier.ordinal();
}
static long nextRefreshAt(String boardId, long now) {
LocalDate tomorrow = Instant.ofEpochMilli(now).atZone(ZoneOffset.UTC).toLocalDate().plusDays(1);
long midnight = tomorrow.atStartOfDay(ZoneOffset.UTC).toInstant().toEpochMilli();
@@ -262,17 +262,17 @@ public final class QuestBoardService {
}
private static void tick(MinecraftServer server) {
QuestBoardData data = QuestBoardData.get(server);
syncLoadedSigns(server, data);
if (++tickCounter < 20) return;
tickCounter = 0;
long now = System.currentTimeMillis();
QuestBoardData data = QuestBoardData.get(server);
for (QuestBoardData.Board board : data.boards()) {
ServerLevel level = level(server, board.dimension());
BlockPos pos = BlockPos.of(board.pos());
if (level != null && level.isLoaded(pos) && !(level.getBlockEntity(pos) instanceof SignBlockEntity)) {
removeBoard(server, data, board, "server", "sign_missing", true); continue;
}
if (level != null && level.isLoaded(pos) && refreshedSignLayouts.add(board.id())) refreshSign(server, board);
QuestBoardData.Contract contract = board.contract().orElse(null);
if (contract != null && contract.claim().isPresent() && now >= contract.claim().get().deadlineAt()) {
QuestBoardData.Claim claim = contract.claim().get();
@@ -288,18 +288,41 @@ public final class QuestBoardService {
}
if (!board.frozen() && now >= board.nextRefreshAt()
&& (contract == null || contract.claim().isEmpty())) {
QuestBoardData.Contract generated = QuestBoardGenerator.generate(server, board.id(), board.questTier(), now);
QuestBoardData.Board refreshed = board.withContract(Optional.of(generated),
QuestBoardGenerator.nextRefreshAt(board.id(), now));
QuestBoardData.Board latest = data.board(board.id()).orElse(null);
if (latest == null || latest.frozen()
|| latest.contract().flatMap(QuestBoardData.Contract::claim).isPresent()) continue;
QuestBoardData.Contract generated = QuestBoardGenerator.generate(server, board.id(), latest.questTier(), now);
QuestBoardData.Board refreshed = latest.withContract(Optional.of(generated),
QuestBoardGenerator.nextRefreshAt(latest.id(), now));
data.replace(refreshed);
data.audit(new QuestBoardData.AuditEntry(now, "refresh", "server", board.id(), generated.id(), "daily"));
data.audit(new QuestBoardData.AuditEntry(now, "refresh", "server", latest.id(), generated.id(), "daily"));
refreshSign(server, refreshed);
}
}
}
private static void syncLoadedSigns(MinecraftServer server, QuestBoardData data) {
for (QuestBoardData.Board board : data.boards()) {
ServerLevel level = level(server, board.dimension());
BlockPos pos = BlockPos.of(board.pos());
boolean loaded = level != null && level.isLoaded(pos);
if (!shouldRewriteSign(refreshedSignLayouts, board.id(), loaded)) continue;
refreshSign(server, board);
}
}
/** A loaded board whose layout was forgotten (chunk unload or failed write) must be rewritten. */
static boolean shouldRewriteSign(Set<String> syncedLayouts, String boardId, boolean chunkLoaded) {
if (!chunkLoaded) {
syncedLayouts.remove(boardId);
return false;
}
return syncedLayouts.add(boardId);
}
private static void removeBoard(MinecraftServer server, QuestBoardData data, QuestBoardData.Board board,
String actor, String reason, boolean refundDeposit) {
refreshedSignLayouts.remove(board.id());
if (!data.remove(board)) return;
long now = System.currentTimeMillis();
board.contract().flatMap(QuestBoardData.Contract::claim).ifPresent(claim -> {
@@ -330,7 +353,8 @@ public final class QuestBoardService {
QuestBoardData.Board board = resolve(data, id);
if (board == null || board.contract().flatMap(QuestBoardData.Contract::claim).isPresent()) return false;
long now = System.currentTimeMillis();
QuestBoardData.Contract contract = QuestBoardGenerator.generate(server, board.id(), board.questTier(), now);
QuestBoardData.Contract contract = QuestBoardGenerator.generate(server, board.id(), board.questTier(), now,
now ^ UUID.randomUUID().getMostSignificantBits());
QuestBoardData.Board updated = board.withContract(Optional.of(contract), QuestBoardGenerator.nextRefreshAt(board.id(), now));
data.replace(updated);
data.audit(new QuestBoardData.AuditEntry(now, "operator_refresh", actor, board.id(), contract.id(), ""));
@@ -338,11 +362,15 @@ public final class QuestBoardService {
return true;
}
static void refreshSign(MinecraftServer server, QuestBoardData.Board board) {
static boolean refreshSign(MinecraftServer server, QuestBoardData.Board board) {
ServerLevel level = level(server, board.dimension());
BlockPos pos = BlockPos.of(board.pos());
if (level != null && level.isLoaded(pos) && level.getBlockEntity(pos) instanceof SignBlockEntity sign)
if (level != null && level.isLoaded(pos) && level.getBlockEntity(pos) instanceof SignBlockEntity sign) {
writeSign(level, sign, board);
return true;
}
refreshedSignLayouts.remove(board.id());
return false;
}
private static void writeSign(ServerLevel level, SignBlockEntity sign, QuestBoardData.Board board) {
@@ -370,6 +398,7 @@ public final class QuestBoardService {
sign.setWaxed(true);
sign.setChanged();
level.sendBlockUpdated(sign.getBlockPos(), sign.getBlockState(), sign.getBlockState(), 3);
refreshedSignLayouts.add(board.id());
}
private static void inspect(ServerPlayer player, QuestBoardData.Board board) {
@@ -1,6 +1,8 @@
package fr.koka99cab.sanctuary26.sanctuary.shop;
import java.util.HashSet;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import net.minecraft.world.item.ItemStack;
@@ -39,6 +41,25 @@ public final class QuestBoardModelSmoke {
long refresh = QuestBoardGenerator.nextRefreshAt("board", now);
require(refresh > now && refresh - now < 28L * 60L * 60L * 1_000L,
"Daily refresh escaped its UTC window");
long day = Math.floorDiv(now, 86_400_000L);
require(QuestBoardGenerator.contractSeed("board", QuestBoardTier.RUBY, day)
== QuestBoardGenerator.contractSeed("board", QuestBoardTier.RUBY, day),
"Daily contracts on the same UTC day must stay deterministic");
require(QuestBoardGenerator.contractSeed("board", QuestBoardTier.RUBY, day)
!= QuestBoardGenerator.contractSeed("board", QuestBoardTier.RUBY, now),
"An operator refresh must not reuse the daily seed");
Set<String> layouts = new HashSet<>();
require(!QuestBoardService.shouldRewriteSign(layouts, "board", false),
"An unloaded board must not rewrite its sign");
require(layouts.isEmpty(), "Unloading must forget the cached sign layout");
require(QuestBoardService.shouldRewriteSign(layouts, "board", true),
"The first load after a rotation must rewrite the sign from saved data");
require(!QuestBoardService.shouldRewriteSign(layouts, "board", true),
"A still-loaded board must not rewrite its sign every tick");
require(!QuestBoardService.shouldRewriteSign(layouts, "board", false),
"Leaving the chunk must forget the layout without rewriting");
require(QuestBoardService.shouldRewriteSign(layouts, "board", true),
"Returning after an unloaded daily refresh must rewrite the new contract");
}
private static void require(boolean value, String message) {