Compare commits
8
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e6740ff891 | ||
|
|
054c8e4ad4 | ||
|
|
4b11d217c2 | ||
|
|
3923b8547c | ||
|
|
76915e2f44 | ||
|
|
5aaaf3831c | ||
|
|
37beda56ec | ||
|
|
5d48917cf4 |
@@ -63,6 +63,12 @@ reçoivent la bande neutre de 96 rubis jusqu’à leur override explicite.
|
||||
- blocs techniques ou créateurs directs de récompenses/farms : spawners, trial spawners, vault,
|
||||
command blocks, structure/test blocks, barrier, light, bedrock et assimilés ;
|
||||
- équipements uniques en titane et les deux monnaies elles-mêmes.
|
||||
- tout le namespace `itsalive:*` afin que cultures, machines, recettes, plats et créatures restent
|
||||
obtenus par le gameplay d’It's Alive ;
|
||||
- les plats vanilla déjà préparés : aliments cuits, soupes et ragoûts, pain, gâteaux, biscuits et
|
||||
tartes ;
|
||||
- `minecraft:enchanted_golden_apple`, qui reste une récompense d'exploration exceptionnelle.
|
||||
|
||||
La même politique bloque ces identifiants à la sortie du générateur de loot, même si un futur
|
||||
résultat aléatoire tente de les sélectionner.
|
||||
Les identifiants économiques, techniques et de lootbox restent également bloqués à la sortie du
|
||||
générateur de loot. Les exclusions culinaires sont propres au Shop : elles empêchent l’achat et
|
||||
la sauvegarde d’une nouvelle offre sans supprimer un colis déjà payé ni les autres sources de jeu.
|
||||
|
||||
+8
-1
@@ -23,6 +23,12 @@ public final class OnlyFunItemPolicy {
|
||||
"nether_portal", "end_portal", "end_gateway", "frosted_ice", "farmland", "dirt_path",
|
||||
"dragon_egg", "filled_map", "written_book", "enchanted_book"
|
||||
);
|
||||
private static final Set<String> VANILLA_SHOP_GAMEPLAY_BYPASSES = Set.of(
|
||||
"baked_potato", "beetroot_soup", "bread", "cake",
|
||||
"cooked_beef", "cooked_chicken", "cooked_cod", "cooked_mutton",
|
||||
"cooked_porkchop", "cooked_rabbit", "cooked_salmon", "cookie", "dried_kelp",
|
||||
"enchanted_golden_apple", "mushroom_stew", "pumpkin_pie", "rabbit_stew", "suspicious_stew"
|
||||
);
|
||||
|
||||
private OnlyFunItemPolicy() {}
|
||||
|
||||
@@ -35,7 +41,8 @@ public final class OnlyFunItemPolicy {
|
||||
if (id == null || isCurrencyOre(id)) return true;
|
||||
String namespace = id.getNamespace().toLowerCase(Locale.ROOT);
|
||||
String path = normalizedPath(id);
|
||||
if (namespace.equals("canaplia")) return true;
|
||||
if (namespace.equals("canaplia") || namespace.equals("itsalive")) return true;
|
||||
if (namespace.equals("minecraft") && VANILLA_SHOP_GAMEPLAY_BYPASSES.contains(path)) return true;
|
||||
if (namespace.equals("anotherworld") && isProtectedAnotherWorldItem(path)) return true;
|
||||
if (TECHNICAL_OR_REWARD_BLOCKS.contains(path) || path.startsWith("infested_")
|
||||
|| path.contains("command_block")) return true;
|
||||
|
||||
@@ -39,7 +39,9 @@ public final class OnlyFunShopCatalog implements ShopCatalogProvider {
|
||||
}
|
||||
|
||||
@Override public boolean isAvailable(MinecraftServer server, String itemId) {
|
||||
return OnlyFunBossShopGate.isAvailable(server, itemId);
|
||||
Identifier id = Identifier.tryParse(itemId);
|
||||
return id != null && !OnlyFunItemPolicy.excludedFromShop(id)
|
||||
&& OnlyFunBossShopGate.isAvailable(server, itemId);
|
||||
}
|
||||
|
||||
/** Registry-derived, deterministic and duplicate-free view of the enabled CSV rows. */
|
||||
|
||||
+21
@@ -2,6 +2,7 @@ package fr.koka99cab.sanctuary26.onlyfun.shop;
|
||||
|
||||
import fr.koka99cab.sanctuary26.onlyfun.policy.OnlyFunItemPolicy;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.shop.ShopOffer;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.shop.ShopOfferCatalog;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
@@ -60,6 +61,26 @@ public final class OnlyFunShopCatalogSmoke {
|
||||
assertConfigured("anotherworld:golden_egg", 1, 2048, 192);
|
||||
assertConfigured("iliketomoveit:magic_carpet", 1, 1536, 128);
|
||||
assertConfigured("iliketomoveit:magic_cloud", 1, 3072, 192);
|
||||
OnlyFunShopCatalog.initialize();
|
||||
for (String excludedId : List.of(
|
||||
"itsalive:apple_pie", "itsalive:basil", "itsalive:culinary_recipe_book",
|
||||
"itsalive:fermentation_barrel", "itsalive:infected_spawn_egg",
|
||||
"minecraft:baked_potato", "minecraft:beetroot_soup", "minecraft:bread",
|
||||
"minecraft:cake", "minecraft:cooked_beef", "minecraft:cooked_chicken",
|
||||
"minecraft:cooked_cod", "minecraft:cooked_mutton", "minecraft:cooked_porkchop",
|
||||
"minecraft:cooked_rabbit", "minecraft:cooked_salmon", "minecraft:cookie",
|
||||
"minecraft:dried_kelp", "minecraft:enchanted_golden_apple", "minecraft:mushroom_stew",
|
||||
"minecraft:pumpkin_pie", "minecraft:rabbit_stew", "minecraft:suspicious_stew")) {
|
||||
Identifier id = Identifier.parse(excludedId);
|
||||
require(OnlyFunItemPolicy.excludedFromShop(id), "Gameplay bypass reached Shop policy: " + id);
|
||||
require(find(all, excludedId) == null, "Gameplay bypass reached Shop catalogue: " + id);
|
||||
require(ShopOfferCatalog.find(null, excludedId) == null,
|
||||
"Server lookup accepted a forbidden saved offer: " + id);
|
||||
}
|
||||
for (String rawFoodId : List.of("minecraft:apple", "minecraft:carrot", "minecraft:potato")) {
|
||||
require(!OnlyFunItemPolicy.excludedFromShop(Identifier.parse(rawFoodId)),
|
||||
"Raw ingredient was removed with finished food: " + rawFoodId);
|
||||
}
|
||||
require(OnlyFunBossShopGate.requiresBoss("iliketomoveit:magic_carpet"),
|
||||
"Magic Carpet must remain boss-gated");
|
||||
require(OnlyFunBossShopGate.requiresBoss("iliketomoveit:magic_cloud"),
|
||||
|
||||
@@ -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
@@ -4,7 +4,9 @@
|
||||
|
||||
Les noms sont déterministes à partir de l'UUID de la créature ; les répétitions sont autorisées.
|
||||
|
||||
- villageois, zombies, moutons, vaches, cochons et poules : prénoms humains comme Patrick, Henri ou Hervé ;
|
||||
- villageois, moutons, vaches, cochons et poules : prénoms humains comme Patrick, Henri ou Hervé ;
|
||||
- toute la famille Zombie, vanilla ou modée : gamertags volontairement « Xbox 360 » comme
|
||||
`xXDarkSasuke69Xx`, `iTzKevin360` ou `xXNoScope420Xx` ;
|
||||
- Endermen : sonorités extraterrestres rendues avec la police alien vanilla `minecraft:alt` ;
|
||||
- Creepers : prénoms et mots en S, avec quelques sifflements ;
|
||||
- squelettes : rythmes et onomatopées musicales ;
|
||||
|
||||
+21
-2
@@ -70,6 +70,22 @@ tasks.register("shopHeaderLayoutSmoke", JavaExec) {
|
||||
mainClass = "fr.koka99cab.sanctuary26.sanctuary.client.ShopDailyHeaderLayoutSmoke"
|
||||
}
|
||||
|
||||
tasks.register("zombieNamesSmoke", JavaExec) {
|
||||
group = "verification"
|
||||
description = "Checks deterministic Xbox-360-style names for every vanilla Zombie subtype."
|
||||
dependsOn tasks.named("testClasses")
|
||||
classpath = sourceSets.test.runtimeClasspath
|
||||
mainClass = "fr.koka99cab.sanctuary26.sanctuary.client.SanctuaryZombieNamesSmoke"
|
||||
}
|
||||
|
||||
tasks.register("capeJoinSyncSmoke", JavaExec) {
|
||||
group = "verification"
|
||||
description = "Checks delayed, bounded cape snapshot retries after player reconnects."
|
||||
dependsOn tasks.named("testClasses")
|
||||
classpath = sourceSets.test.runtimeClasspath
|
||||
mainClass = "fr.koka99cab.sanctuary26.sanctuary.gameplay.CapeJoinSyncQueueSmoke"
|
||||
}
|
||||
|
||||
tasks.register("verifyBlackMarket") {
|
||||
group = "verification"
|
||||
description = "Checks the physical sign market, flash Shop, escrow ledger and mailbox delivery."
|
||||
@@ -105,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")
|
||||
}
|
||||
@@ -220,7 +238,8 @@ tasks.register("verifySanctuary") {
|
||||
inputs.files(fileTree("tools"))
|
||||
dependsOn tasks.named("progressionModelSmoke"), tasks.named("serverIdentityModelSmoke"),
|
||||
tasks.named("shopDeliveryTimingSmoke"), tasks.named("blackMarketModelSmoke"),
|
||||
tasks.named("questBoardModelSmoke"), tasks.named("shopHeaderLayoutSmoke"),
|
||||
tasks.named("questBoardModelSmoke"), tasks.named("shopHeaderLayoutSmoke"), tasks.named("zombieNamesSmoke"),
|
||||
tasks.named("capeJoinSyncSmoke"),
|
||||
tasks.named("verifyBlackMarket")
|
||||
|
||||
doLast {
|
||||
|
||||
@@ -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();
|
||||
|
||||
+18
-1
@@ -33,6 +33,15 @@ public final class SanctuaryMobNames {
|
||||
"Raymond", "Colette", "Norbert", "Mireille", "Didier", "Chantal", "Marcel", "Brigitte",
|
||||
"Jean-Michel", "Françoise", "Pascal", "Sylvie", "René", "Martine", "Gilbert", "Odette"
|
||||
};
|
||||
private static final String[] ZOMBIE_GAMERTAGS = {
|
||||
"xXDarkSasuke69Xx", "xXSniperDu59Xx", "OoKilleur93oO", "iTzKevin360",
|
||||
"xXNoScope420Xx", "DarkKiller2009", "LeBGduMinecraft26", "xXShadowNinja76Xx",
|
||||
"MLGRaptor67", "FazeKevinDu13", "QuickScope360", "NarutoPro59",
|
||||
"xXGhostRider666Xx", "TheDarkCreeper42", "L3G3NDKILL3R", "xXEnderBoss77Xx",
|
||||
"SkyZzOfficiel62", "xXPikaKiller25Xx", "DarkAngel666", "iiTzWarrior360ii",
|
||||
"xXDragonFeu974Xx", "ProGamerDu59", "xXHeadShot360Xx", "BoGoss2008",
|
||||
"RoiDuNoScope93", "xXDeathNinja13Xx", "TeamDark59", "KevinLeBoss360"
|
||||
};
|
||||
private static final String[] ENDER = {
|
||||
"Xyr-9", "Vaeluun", "Orryx", "Nhal", "Zyphra", "Qor'eth", "Uulma", "Thess-7",
|
||||
"Ixxa", "Vhoor", "Aen-Null", "Kryss", "Omnû", "Yl'zar", "Nexil", "Rhaum"
|
||||
@@ -92,6 +101,7 @@ public final class SanctuaryMobNames {
|
||||
}
|
||||
|
||||
static String generate(Entity entity) {
|
||||
if (usesZombieGamertags(entity.getClass())) return zombieName(entity.getUUID());
|
||||
String[] family = switch (entity) {
|
||||
case EnderMan ignored -> ENDER;
|
||||
case Creeper ignored -> CREEPERS;
|
||||
@@ -99,7 +109,6 @@ public final class SanctuaryMobNames {
|
||||
case Wolf ignored -> DOGS;
|
||||
case Cat ignored -> CATS;
|
||||
case Villager ignored -> HUMANS;
|
||||
case Zombie ignored -> HUMANS;
|
||||
case Cow ignored -> HUMANS;
|
||||
case Pig ignored -> HUMANS;
|
||||
case Sheep ignored -> HUMANS;
|
||||
@@ -112,6 +121,14 @@ public final class SanctuaryMobNames {
|
||||
return pick(family, entity.getUUID());
|
||||
}
|
||||
|
||||
static boolean usesZombieGamertags(Class<?> entityClass) {
|
||||
return entityClass != null && Zombie.class.isAssignableFrom(entityClass);
|
||||
}
|
||||
|
||||
static String zombieName(UUID uuid) {
|
||||
return pick(ZOMBIE_GAMERTAGS, uuid);
|
||||
}
|
||||
|
||||
private static String pick(String[] values, UUID uuid) {
|
||||
long mixed = uuid.getMostSignificantBits() ^ Long.rotateLeft(uuid.getLeastSignificantBits(), 23);
|
||||
mixed = (mixed ^ (mixed >>> 30)) * 0xBF58476D1CE4E5B9L;
|
||||
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
package fr.koka99cab.sanctuary26.sanctuary.gameplay;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
/** Bounded reconnect retry state for a player's complete cape snapshot. */
|
||||
final class CapeJoinSyncQueue {
|
||||
enum Decision { WAIT, SYNC, DROP }
|
||||
|
||||
private final int retryTicks;
|
||||
private final Map<UUID, Integer> remainingTicks = new LinkedHashMap<>();
|
||||
|
||||
CapeJoinSyncQueue(int retryTicks) {
|
||||
if (retryTicks < 1) throw new IllegalArgumentException("Cape retry window must be positive");
|
||||
this.retryTicks = retryTicks;
|
||||
}
|
||||
|
||||
void schedule(UUID playerId) {
|
||||
remainingTicks.put(playerId, retryTicks);
|
||||
}
|
||||
|
||||
List<UUID> pendingPlayerIds() {
|
||||
return List.copyOf(remainingTicks.keySet());
|
||||
}
|
||||
|
||||
Decision poll(UUID playerId, boolean connected, boolean channelReady) {
|
||||
Integer remaining = remainingTicks.get(playerId);
|
||||
if (remaining == null) return Decision.DROP;
|
||||
if (!connected) {
|
||||
remainingTicks.remove(playerId);
|
||||
return Decision.DROP;
|
||||
}
|
||||
if (channelReady) {
|
||||
remainingTicks.remove(playerId);
|
||||
return Decision.SYNC;
|
||||
}
|
||||
if (remaining <= 1) {
|
||||
remainingTicks.remove(playerId);
|
||||
return Decision.DROP;
|
||||
}
|
||||
remainingTicks.put(playerId, remaining - 1);
|
||||
return Decision.WAIT;
|
||||
}
|
||||
}
|
||||
+28
@@ -1,13 +1,18 @@
|
||||
package fr.koka99cab.sanctuary26.sanctuary.gameplay;
|
||||
|
||||
import fr.koka99cab.sanctuary26.sanctuary.network.CapeSyncPayload;
|
||||
import java.util.UUID;
|
||||
import net.fabricmc.fabric.api.entity.event.v1.ServerPlayerEvents;
|
||||
import net.fabricmc.fabric.api.event.lifecycle.v1.ServerTickEvents;
|
||||
import net.fabricmc.fabric.api.networking.v1.PlayerLookup;
|
||||
import net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
|
||||
/** Server authority and multiplayer distribution for equipped cape visuals. */
|
||||
public final class SanctuaryCapeService {
|
||||
private static final int JOIN_SYNC_RETRY_TICKS = 100;
|
||||
private static final CapeJoinSyncQueue JOIN_SYNCS = new CapeJoinSyncQueue(JOIN_SYNC_RETRY_TICKS);
|
||||
private static boolean initialized;
|
||||
|
||||
private SanctuaryCapeService() {
|
||||
@@ -17,6 +22,7 @@ public final class SanctuaryCapeService {
|
||||
if (initialized) return;
|
||||
initialized = true;
|
||||
ServerPlayerEvents.AFTER_RESPAWN.register((oldPlayer, newPlayer, alive) -> sync(newPlayer));
|
||||
ServerTickEvents.END_SERVER_TICK.register(SanctuaryCapeService::tickJoinSyncs);
|
||||
}
|
||||
|
||||
public static void join(ServerPlayer joined) {
|
||||
@@ -24,6 +30,10 @@ public final class SanctuaryCapeService {
|
||||
send(joined, online);
|
||||
}
|
||||
sync(joined);
|
||||
// JOIN can run while the reconnecting client's play channel or player list is
|
||||
// still settling. Replay the complete snapshot on a later server tick so a
|
||||
// missed initial packet never requires removing and re-equipping the cape.
|
||||
JOIN_SYNCS.schedule(joined.getUUID());
|
||||
}
|
||||
|
||||
public static void sync(ServerPlayer owner) {
|
||||
@@ -32,6 +42,24 @@ public final class SanctuaryCapeService {
|
||||
}
|
||||
}
|
||||
|
||||
private static void tickJoinSyncs(MinecraftServer server) {
|
||||
for (UUID playerId : JOIN_SYNCS.pendingPlayerIds()) {
|
||||
ServerPlayer receiver = server.getPlayerList().getPlayer(playerId);
|
||||
CapeJoinSyncQueue.Decision decision = JOIN_SYNCS.poll(playerId, receiver != null,
|
||||
receiver != null && ServerPlayNetworking.canSend(receiver, CapeSyncPayload.ID));
|
||||
if (decision == CapeJoinSyncQueue.Decision.SYNC) sendSnapshot(receiver);
|
||||
}
|
||||
}
|
||||
|
||||
private static void sendSnapshot(ServerPlayer receiver) {
|
||||
boolean includedReceiver = false;
|
||||
for (ServerPlayer owner : PlayerLookup.all(receiver.level().getServer())) {
|
||||
send(receiver, owner);
|
||||
includedReceiver |= owner.getUUID().equals(receiver.getUUID());
|
||||
}
|
||||
if (!includedReceiver) send(receiver, receiver);
|
||||
}
|
||||
|
||||
private static void send(ServerPlayer receiver, ServerPlayer owner) {
|
||||
if (!ServerPlayNetworking.canSend(receiver, CapeSyncPayload.ID)) return;
|
||||
String capeId = SanctuaryCapes.itemId(SanctuaryInventoryCapacity.equippedCape(owner));
|
||||
|
||||
+7
@@ -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();
|
||||
}
|
||||
|
||||
+12
-34
@@ -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;
|
||||
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
package fr.koka99cab.sanctuary26.sanctuary.client;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import net.minecraft.world.entity.monster.Creeper;
|
||||
import net.minecraft.world.entity.monster.zombie.Drowned;
|
||||
import net.minecraft.world.entity.monster.zombie.Husk;
|
||||
import net.minecraft.world.entity.monster.zombie.Zombie;
|
||||
import net.minecraft.world.entity.monster.zombie.ZombieVillager;
|
||||
import net.minecraft.world.entity.monster.zombie.ZombifiedPiglin;
|
||||
|
||||
/** Executable coverage for deterministic Xbox-360-style names across the complete Zombie family. */
|
||||
public final class SanctuaryZombieNamesSmoke {
|
||||
private SanctuaryZombieNamesSmoke() {
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
for (Class<?> zombieClass : new Class<?>[] {
|
||||
Zombie.class, Husk.class, Drowned.class, ZombieVillager.class, ZombifiedPiglin.class
|
||||
}) {
|
||||
require(SanctuaryMobNames.usesZombieGamertags(zombieClass),
|
||||
"Zombie subtype missed gamertag family: " + zombieClass.getSimpleName());
|
||||
}
|
||||
require(!SanctuaryMobNames.usesZombieGamertags(Creeper.class),
|
||||
"Non-zombie monster entered gamertag family");
|
||||
|
||||
UUID stableId = UUID.fromString("12345678-1234-5678-9abc-def012345678");
|
||||
require(SanctuaryMobNames.zombieName(stableId).equals(SanctuaryMobNames.zombieName(stableId)),
|
||||
"Zombie gamertag is not deterministic");
|
||||
Set<String> observed = new HashSet<>();
|
||||
for (int index = 0; index < 4_096; index++) {
|
||||
String name = SanctuaryMobNames.zombieName(new UUID(index * 31L, ~((long) index * 97L)));
|
||||
require(name.matches("[A-Za-z0-9_]+"), "Gamertag contains an invalid character: " + name);
|
||||
require(name.chars().anyMatch(Character::isDigit), "Gamertag has no Xbox-style digits: " + name);
|
||||
observed.add(name);
|
||||
}
|
||||
require(observed.size() >= 24, "Zombie gamertag pool is too small: " + observed.size());
|
||||
}
|
||||
|
||||
private static void require(boolean condition, String message) {
|
||||
if (!condition) throw new IllegalStateException(message);
|
||||
}
|
||||
}
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
package fr.koka99cab.sanctuary26.sanctuary.gameplay;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
/** Executable coverage for delayed and bounded cape synchronization after reconnect. */
|
||||
public final class CapeJoinSyncQueueSmoke {
|
||||
private CapeJoinSyncQueueSmoke() {
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
CapeJoinSyncQueue queue = new CapeJoinSyncQueue(3);
|
||||
UUID playerId = UUID.fromString("12345678-1234-5678-9abc-def012345678");
|
||||
|
||||
queue.schedule(playerId);
|
||||
require(queue.pendingPlayerIds().size() == 1, "Reconnect did not schedule one cape snapshot");
|
||||
require(queue.poll(playerId, true, false) == CapeJoinSyncQueue.Decision.WAIT,
|
||||
"Unavailable play channel was not retried");
|
||||
queue.schedule(playerId);
|
||||
require(queue.pendingPlayerIds().size() == 1, "Repeated join duplicated the cape retry");
|
||||
require(queue.poll(playerId, true, true) == CapeJoinSyncQueue.Decision.SYNC,
|
||||
"Ready play channel did not release the cape snapshot");
|
||||
require(queue.pendingPlayerIds().isEmpty(), "Successful cape sync stayed pending");
|
||||
|
||||
queue.schedule(playerId);
|
||||
require(queue.poll(playerId, false, false) == CapeJoinSyncQueue.Decision.DROP,
|
||||
"Disconnected player retained a cape retry");
|
||||
|
||||
queue.schedule(playerId);
|
||||
require(queue.poll(playerId, true, false) == CapeJoinSyncQueue.Decision.WAIT,
|
||||
"Cape retry window ended one tick too early");
|
||||
require(queue.poll(playerId, true, false) == CapeJoinSyncQueue.Decision.WAIT,
|
||||
"Cape retry window ended two ticks too early");
|
||||
require(queue.poll(playerId, true, false) == CapeJoinSyncQueue.Decision.DROP,
|
||||
"Cape retry escaped its bounded window");
|
||||
require(queue.pendingPlayerIds().isEmpty(), "Expired cape retry stayed pending");
|
||||
}
|
||||
|
||||
private static void require(boolean condition, String message) {
|
||||
if (!condition) throw new IllegalStateException(message);
|
||||
}
|
||||
}
|
||||
+6
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user