fix(sanctuary): réserver les requêtes aux panneaux dédiés (#25) #67

Merged
koka merged 1 commits from fix/request-sign-block-only into main 2026-08-29 11:11:41 +00:00
7 changed files with 41 additions and 44 deletions
+10 -5
View File
@@ -60,15 +60,20 @@ Les quêtes possèdent leur propre sauvegarde (`black_market_quests`, version 1)
panneau actif contient un contrat réel : ajouter des panneaux augmente donc le nombre
de quêtes quotidiennes disponibles.
Activation d'un panneau vanilla libre :
Création d'un panneau de quête serveur :
1. Tenir huit gemmes du niveau voulu : émeraude pour facile, rubis pour moyen, saphir
pour difficile.
2. S'accroupir et faire clic droit sur le panneau. Les huit gemmes sont consommées et
le panneau est protégé au nom du joueur qui l'a créé.
1. Fabriquer le panneau dédié du niveau voulu autour d'un panneau vanilla : huit
émeraudes pour facile, huit rubis pour moyen ou huit saphirs pour difficile.
2. Poser ce panneau dédié. Il devient immédiatement un panneau de quête protégé au nom
du joueur qui l'a créé.
3. Le renouvellement a lieu une fois par jour réel, autour de minuit UTC avec un
décalage déterministe compris entre zéro et deux heures.
Un panneau vanilla libre ne peut plus être converti en panneau de quête avec des gemmes.
Le panneau rose `sanctuary:quest_request_sign` reste le seul support des demandes
communautaires. Les anciens panneaux vanilla qui possèdent déjà un contrat enregistré
restent utilisables jusqu'à leur destruction afin de préserver les réservations existantes.
Interactions :
- clic droit : réserver une quête libre, puis livrer ou vérifier sa propre quête ;
+3 -1
View File
@@ -121,7 +121,9 @@ tasks.register("verifyBlackMarket") {
|| !service.contains("currencyStacks")
|| !questData.contains('SAVE_KEY = "black_market_quests"')
|| !quests.contains("ServerLivingEntityEvents.AFTER_DEATH")
|| !quests.contains("QuestBoardTier.ACTIVATION_COST")
|| !quests.contains("SanctuaryRegistries.questBoardTier")
|| quests.contains("activationTier(")
|| quests.contains("QuestBoardTier.ACTIVATION_COST")
|| !quests.contains("addTreasury(claim.depositCurrency(), claim.depositAmount())")) {
throw new GradleException("The physical Black Market escrow/delivery contract is incomplete")
}
@@ -62,8 +62,8 @@ public final class SanctuaryMod implements ModInitializer {
SanctuaryProgressionGates.initialize();
SanctuaryPlayerColors.initialize();
ShopService.initialize();
// Community request signs must claim their interactions before the generic
// gem activation callback used by ordinary signs.
// Community request signs must claim their interactions before the other
// physical sign services.
QuestRequestService.initialize();
QuestBoardService.initialize();
BlackMarketService.initialize();
@@ -99,6 +99,13 @@ public final class SanctuaryRegistries {
return java.util.List.of(EMERALD_QUEST_SIGN, RUBY_QUEST_SIGN, SAPPHIRE_QUEST_SIGN);
}
public static QuestBoardTier questBoardTier(Block block) {
for (QuestSignSet signs : questSigns()) {
if (block == signs.standing() || block == signs.wall()) return signs.tier();
}
return null;
}
public static boolean isQuestRequestSign(Block block) {
return block == QUEST_REQUEST_SIGN.standing() || block == QUEST_REQUEST_SIGN.wall();
}
@@ -1,6 +1,7 @@
package fr.koka99cab.sanctuary26.sanctuary.shop;
import fr.koka99cab.sanctuary26.sanctuary.api.inventory.SanctuaryPlayerInventoryView;
import fr.koka99cab.sanctuary26.sanctuary.registry.SanctuaryRegistries;
import java.time.Duration;
import java.util.ArrayList;
import java.util.HashSet;
@@ -50,14 +51,9 @@ public final class QuestBoardService {
UseBlockCallback.EVENT.register((player, level, hand, hit) -> {
if (hand != InteractionHand.MAIN_HAND || !(level.getBlockEntity(hit.getBlockPos()) instanceof SignBlockEntity sign))
return InteractionResult.PASS;
QuestBoardTier heldTier = activationTier(player.getMainHandItem());
if (level.isClientSide()) {
return player.isSecondaryUseActive() && heldTier != null
&& player.getMainHandItem().getCount() >= QuestBoardTier.ACTIVATION_COST
? InteractionResult.SUCCESS : InteractionResult.PASS;
}
if (level.isClientSide()) return InteractionResult.PASS;
return player instanceof ServerPlayer serverPlayer
? interact(serverPlayer, (ServerLevel) level, hit.getBlockPos(), sign, heldTier)
? interact(serverPlayer, (ServerLevel) level, hit.getBlockPos(), sign)
: InteractionResult.PASS;
});
PlayerBlockBreakEvents.BEFORE.register((level, player, pos, state, blockEntity) -> {
@@ -81,34 +77,19 @@ public final class QuestBoardService {
}
private static InteractionResult interact(ServerPlayer player, ServerLevel level, BlockPos pos,
SignBlockEntity sign, QuestBoardTier heldTier) {
SignBlockEntity sign) {
QuestBoardData data = QuestBoardData.get(level.getServer());
QuestBoardData.Board board = data.boardAt(dimension(level), pos.asLong()).orElse(null);
if (board != null) {
handleBoard(player, level, sign, data, board);
return InteractionResult.SUCCESS_SERVER;
}
if (!player.isSecondaryUseActive() || heldTier == null
|| player.getMainHandItem().getCount() < QuestBoardTier.ACTIVATION_COST) return InteractionResult.PASS;
if (BlackMarketData.get(level.getServer()).listingAt(dimension(level), pos.asLong()).isPresent()) {
message(player, "occupied"); return InteractionResult.CONSUME;
}
if (BlackMarketData.get(level.getServer()).isBanned(player.getUUID())) {
player.sendSystemMessage(Component.translatable("message.sanctuary.black_market.banned"));
return InteractionResult.CONSUME;
}
ItemStack held = player.getMainHandItem();
held.shrink(QuestBoardTier.ACTIVATION_COST);
player.getInventory().setChanged();
if (!createBoard(player, level, pos, sign, heldTier)) {
held.grow(QuestBoardTier.ACTIVATION_COST); message(player, "occupied"); return InteractionResult.CONSUME;
}
if (board == null) return InteractionResult.PASS;
handleBoard(player, level, sign, data, board);
return InteractionResult.SUCCESS_SERVER;
}
/** Activates one of the three crafted quest signs without charging its material cost twice. */
public static void activatePlacedSign(ServerPlayer player, ServerLevel level, BlockPos pos,
SignBlockEntity sign, QuestBoardTier tier) {
QuestBoardTier placedTier = SanctuaryRegistries.questBoardTier(level.getBlockState(pos).getBlock());
if (!canActivateDedicatedSign(placedTier, tier)) return;
if (BlackMarketData.get(level.getServer()).isBanned(player.getUUID())) {
player.sendSystemMessage(Component.translatable("message.sanctuary.black_market.banned"));
return;
@@ -116,6 +97,10 @@ public final class QuestBoardService {
if (!createBoard(player, level, pos, sign, tier)) message(player, "occupied");
}
static boolean canActivateDedicatedSign(QuestBoardTier placedTier, QuestBoardTier requestedTier) {
return placedTier != null && placedTier == requestedTier;
}
private static boolean createBoard(ServerPlayer player, ServerLevel level, BlockPos pos,
SignBlockEntity sign, QuestBoardTier tier) {
QuestBoardData data = QuestBoardData.get(level.getServer());
@@ -466,13 +451,6 @@ public final class QuestBoardService {
if (item != null) player.getInventory().placeItemBackInInventory(new ItemStack(item, deposit.amount));
}
private static QuestBoardTier activationTier(ItemStack stack) {
if (stack.isEmpty()) return null;
Identifier id = BuiltInRegistries.ITEM.getKey(stack.getItem());
for (QuestBoardTier tier : QuestBoardTier.values()) if (tier.currencyItem().equals(id)) return tier;
return null;
}
private static ServerPlayer killer(LivingEntity entity, DamageSource source) {
Entity attacker = source.getEntity();
if (attacker instanceof ServerPlayer player) return player;
@@ -5,13 +5,12 @@ import java.util.Locale;
import net.minecraft.ChatFormatting;
import net.minecraft.resources.Identifier;
/** Physical quest-board tiers. The activation material is also the default refundable bond. */
/** Physical quest-board tiers. Each tier currency is also its default refundable bond. */
public enum QuestBoardTier {
EMERALD("emerald", Identifier.withDefaultNamespace("emerald"), 1, Duration.ofMinutes(15), ChatFormatting.GREEN),
RUBY("ruby", Identifier.fromNamespaceAndPath("anotherworld", "ruby"), 1, Duration.ofMinutes(45), ChatFormatting.RED),
SAPPHIRE("sapphire", Identifier.fromNamespaceAndPath("anotherworld", "sapphire"), 1, Duration.ofMinutes(90), ChatFormatting.AQUA);
public static final int ACTIVATION_COST = 8;
private final String id;
private final Identifier currencyItem;
private final int deposit;
@@ -29,6 +29,12 @@ public final class QuestBoardModelSmoke {
require(reserved.withObjective(objective.advance(20)).objective().complete(), "Kill progress did not complete");
require(QuestBoardTier.SAPPHIRE.reservationMillis() > QuestBoardTier.RUBY.reservationMillis(),
"Difficulty deadline order is invalid");
require(!QuestBoardService.canActivateDedicatedSign(null, QuestBoardTier.EMERALD),
"A vanilla sign can still become a quest board");
require(!QuestBoardService.canActivateDedicatedSign(QuestBoardTier.EMERALD, QuestBoardTier.RUBY),
"A mismatched dedicated sign accepted the wrong quest tier");
require(QuestBoardService.canActivateDedicatedSign(QuestBoardTier.SAPPHIRE, QuestBoardTier.SAPPHIRE),
"A dedicated quest sign rejected its own tier");
long now = 1_800_000_000_000L;
long refresh = QuestBoardGenerator.nextRefreshAt("board", now);
require(refresh > now && refresh - now < 28L * 60L * 60L * 1_000L,