Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
14980c993f | ||
|
|
e17433f28f | ||
|
|
91b4def490 |
+4
-1
@@ -8,7 +8,7 @@ Ce journal suit les **versions du modpack**. Lorsqu'un seul module change, sa ve
|
||||
|
||||
## Non distribué
|
||||
|
||||
Modules modifiés : **It's Alive `0.0.0-alpha.36`**, **Sanctuary `0.0.0-alpha.119`**, **Ambiance `0.0.0-alpha.15`**, **Only Fun `0.0.0-alpha.23`** et **Ouch `0.0.0-alpha.10`**. Le pack distribué reste en `26.2.0-alpha.210` jusqu’à la prochaine release.
|
||||
Modules modifiés : **It's Alive `0.0.0-alpha.36`**, **Sanctuary `0.0.0-alpha.119`**, **Ambiance `0.0.0-alpha.15`**, **Only Fun `0.0.0-alpha.24`** et **Ouch `0.0.0-alpha.10`**. Le pack distribué reste en `26.2.0-alpha.210` jusqu’à la prochaine release.
|
||||
|
||||
- remplace les pages noires du Livre interdit par l’affichage clair du livre vanilla ;
|
||||
- conserve les seize entrées, les découvertes personnelles, le partage sur pupitre, les IDs, les données v3 et le protocole réseau 4.
|
||||
@@ -23,6 +23,9 @@ Modules modifiés : **It's Alive `0.0.0-alpha.36`**, **Sanctuary `0.0.0-alpha.11
|
||||
- rend `abjection:shit_ball` lançable à la main et par dispenser grâce à une fabrique publique installée par Ouch, sans réenregistrer l’objet possédé par Only Fun ;
|
||||
- restaure le projectile historique de 0,25 bloc, sa consommation unique hors créatif, son rendu d’objet, son impact boueux et ses sept secondes de nausée ajoutées à la durée existante ;
|
||||
- déclare la dépendance sans cycle `ouch -> onlyfun` ; les données et protocoles réseau d’Ouch et d’Only Fun restent inchangés.
|
||||
- ajoute la touche configurable « Chier » dans les contrôles Only Fun, assignée à `J` par défaut et sans répétition lorsque la touche reste maintenue ;
|
||||
- transmet une demande C2S vide, déduplique le spam d’un même tick et revalide côté serveur la connexion, la vie et le mode spectateur avant de réutiliser exactement l’action `/shit` ;
|
||||
- conserve les données Only Fun v2 et porte son protocole réseau de 2 à 3, sans nouveau cooldown, besoin ni donnée persistante.
|
||||
|
||||
## `26.2.0-alpha.210`
|
||||
|
||||
|
||||
+31
-9
@@ -3439,7 +3439,7 @@ tasks.register("verifyProgressivePauseMenuRelease") {
|
||||
}
|
||||
if (rootProject.pack_version != "26.2.0-alpha.210"
|
||||
|| rootProject.sanctuary_version != "0.0.0-alpha.119"
|
||||
|| rootProject.onlyfun_version != "0.0.0-alpha.23"
|
||||
|| rootProject.onlyfun_version != "0.0.0-alpha.24"
|
||||
|| release.pack_version != rootProject.pack_version || index.versionId != rootProject.pack_version
|
||||
|| migration.source?.modules != [sanctuary: "0.0.0-alpha.108", onlyfun: "0.0.0-alpha.17"]
|
||||
|| migration.target?.modules != [sanctuary: "0.0.0-alpha.109", onlyfun: "0.0.0-alpha.17"]
|
||||
@@ -3645,9 +3645,17 @@ project(":onlyfun") {
|
||||
mainClass = "fr.koka99cab.sanctuary26.onlyfun.api.abjection.AbjectionActionContractSmoke"
|
||||
}
|
||||
|
||||
tasks.register("shitActionRequestSmoke", JavaExec) {
|
||||
group = "verification"
|
||||
description = "Checks one-shot and same-tick deduplication for the Abjection key request."
|
||||
dependsOn tasks.named("testClasses")
|
||||
classpath = sourceSets.test.runtimeClasspath
|
||||
mainClass = "fr.koka99cab.sanctuary26.onlyfun.abjection.ShitActionRequestSmoke"
|
||||
}
|
||||
|
||||
tasks.register("verifyOnlyFun") {
|
||||
group = "verification"
|
||||
description = "Checks Only Fun alpha.22, including its public Abjection projectile contract."
|
||||
description = "Checks Only Fun alpha.24, including the bounded configurable Abjection key."
|
||||
inputs.files(fileTree("src/main/java"))
|
||||
inputs.files(fileTree("src/main/resources"))
|
||||
inputs.file(file("art/shop/shop_catalog_editable.csv"))
|
||||
@@ -3741,10 +3749,10 @@ project(":onlyfun") {
|
||||
def shitCubeRecipe = new JsonSlurper().parse(file(
|
||||
"src/main/resources/data/abjection/recipe/shit_cube.json"))
|
||||
if (manifest.custom.sanctuary26.data_version != 2
|
||||
|| project.version.toString() != "0.0.0-alpha.23"
|
||||
|| project.version.toString() != "0.0.0-alpha.24"
|
||||
|| manifest.depends?.sanctuary != "*"
|
||||
|| manifest.provides != ["abjection"]
|
||||
|| manifest.custom.sanctuary26.network_protocol != 2
|
||||
|| manifest.custom.sanctuary26.network_protocol != 3
|
||||
|| !javaText.contains('NAMESPACE = "abjection"')
|
||||
|| !javaText.contains('public static StartResult start(ServerPlayer player, Action action)')
|
||||
|| !javaText.contains('PISS(15 * 20, "piss")')
|
||||
@@ -3758,6 +3766,13 @@ project(":onlyfun") {
|
||||
|| !javaText.contains('class ShitBallItem extends Item implements ProjectileItem')
|
||||
|| !javaText.contains('public static void registerFactory(Factory implementation)')
|
||||
|| !javaText.contains('return AbjectionProjectileApi.create(level, position, stack, direction)')
|
||||
|| !javaText.contains('id("c2s/shit_action")')
|
||||
|| !javaText.contains('CustomPacketPayload.codec((payload, buffer) -> {},')
|
||||
|| !javaText.contains('GLFW.GLFW_KEY_J')
|
||||
|| !javaText.contains('while (shitKey.consumeClick())')
|
||||
|| !javaText.contains('server.getPlayerList().getPlayer(player.getUUID()) != player')
|
||||
|| !javaText.contains('acceptTick(player.getUUID(), server.getTickCount())')
|
||||
|| !javaText.contains('AbjectionActions.start(player, AbjectionActions.Action.SHIT)')
|
||||
|| shitCubeTexture == null || shitCubeTexture.width != 16 || shitCubeTexture.height != 16
|
||||
|| shitBallTexture == null || shitBallTexture.width != 16 || shitBallTexture.height != 16
|
||||
|| shitCubeRecipe.key?.get("#") != "abjection:shit_ball"
|
||||
@@ -3772,6 +3787,12 @@ project(":onlyfun") {
|
||||
translations.containsKey(it)
|
||||
}
|
||||
}
|
||||
|| ["fr_fr", "en_us", "ru_ru"].any { locale ->
|
||||
def translations = new JsonSlurper().parse(file(
|
||||
"src/main/resources/assets/onlyfun/lang/${locale}.json"))
|
||||
!translations.containsKey("key.categories.onlyfun.onlyfun")
|
||||
|| !translations.containsKey("key.onlyfun.shit")
|
||||
}
|
||||
|| !javaText.contains("ANOMALY_DENOMINATOR = 256")
|
||||
|| !javaText.contains("PITY_START = 24")
|
||||
|| !javaText.contains("class LootboxItem")
|
||||
@@ -3919,6 +3940,7 @@ project(":onlyfun") {
|
||||
|
||||
tasks.named("check") {
|
||||
dependsOn tasks.named("abjectionActionContractSmoke")
|
||||
dependsOn tasks.named("shitActionRequestSmoke")
|
||||
dependsOn tasks.named("onlyFunGeneratorSmoke")
|
||||
dependsOn tasks.named("verifyOnlyFun")
|
||||
}
|
||||
@@ -4185,7 +4207,7 @@ tasks.register("verifyShopPricingLayoutRelease") {
|
||||
def release = new JsonSlurper().parse(file("pack/release.json"))
|
||||
def index = new JsonSlurper().parse(file("pack/prism/modrinth.index.json"))
|
||||
if (rootProject.pack_version != "26.2.0-alpha.210"
|
||||
|| rootProject.onlyfun_version != "0.0.0-alpha.23"
|
||||
|| rootProject.onlyfun_version != "0.0.0-alpha.24"
|
||||
|| rootProject.sanctuary_version != "0.0.0-alpha.119"
|
||||
|| release.pack_version != rootProject.pack_version || index.versionId != rootProject.pack_version
|
||||
|| migration.source?.pack_version != "26.2.0-alpha.192"
|
||||
@@ -4279,7 +4301,7 @@ tasks.register("verifyShopCsvBossGatesRelease") {
|
||||
def catalog = file("onlyfun/src/main/java/fr/koka99cab/sanctuary26/onlyfun/shop/OnlyFunShopCatalog.java").text
|
||||
def offerCatalog = file("sanctuary/src/main/java/fr/koka99cab/sanctuary26/sanctuary/shop/ShopOfferCatalog.java").text
|
||||
if (rootProject.pack_version != "26.2.0-alpha.210"
|
||||
|| rootProject.onlyfun_version != "0.0.0-alpha.23"
|
||||
|| rootProject.onlyfun_version != "0.0.0-alpha.24"
|
||||
|| rootProject.sanctuary_version != "0.0.0-alpha.119"
|
||||
|| release.pack_version != rootProject.pack_version || index.versionId != rootProject.pack_version
|
||||
|| migration.source?.pack_version != "26.2.0-alpha.194"
|
||||
@@ -5548,7 +5570,7 @@ tasks.register("verifyCanapliaAlpha209Release") {
|
||||
if (rootProject.pack_version != "26.2.0-alpha.210"
|
||||
|| rootProject.ambiance_version != "0.0.0-alpha.15"
|
||||
|| rootProject.itsalive_version != "0.0.0-alpha.36"
|
||||
|| rootProject.onlyfun_version != "0.0.0-alpha.23"
|
||||
|| rootProject.onlyfun_version != "0.0.0-alpha.24"
|
||||
|| migration.source?.pack_version != "26.2.0-alpha.207"
|
||||
|| migration.source?.modules != expectedSource
|
||||
|| migration.target?.pack_version != "26.2.0-alpha.208"
|
||||
@@ -5656,7 +5678,7 @@ tasks.register("verifyAlpha210Release") {
|
||||
}
|
||||
if (rootProject.pack_version != "26.2.0-alpha.210"
|
||||
|| rootProject.sanctuary_version != "0.0.0-alpha.119"
|
||||
|| rootProject.onlyfun_version != "0.0.0-alpha.23"
|
||||
|| rootProject.onlyfun_version != "0.0.0-alpha.24"
|
||||
|| rootProject.iliketomoveit_version != expectedTarget.iliketomoveit
|
||||
|| rootProject.itsalive_version != "0.0.0-alpha.36"
|
||||
|| migration.source?.pack_version != "26.2.0-alpha.209"
|
||||
@@ -5695,7 +5717,7 @@ tasks.register("verifyAlpha210Release") {
|
||||
|| manifests.sanctuary.custom?.sanctuary26?.data_version != 11
|
||||
|| manifests.sanctuary.custom?.sanctuary26?.network_protocol != 22
|
||||
|| manifests.onlyfun.custom?.sanctuary26?.data_version != 2
|
||||
|| manifests.onlyfun.custom?.sanctuary26?.network_protocol != 2
|
||||
|| manifests.onlyfun.custom?.sanctuary26?.network_protocol != 3
|
||||
|| manifests.iliketomoveit.custom?.sanctuary26?.data_version != 3
|
||||
|| manifests.iliketomoveit.custom?.sanctuary26?.network_protocol != 5
|
||||
|| manifests.itsalive.custom?.sanctuary26?.data_version != 3
|
||||
|
||||
+1
-1
@@ -26,7 +26,7 @@ masterkey_version=0.0.0-alpha.8
|
||||
masterkey_lifecycle=active
|
||||
iliketomoveit_version=0.0.0-alpha.15
|
||||
iliketomoveit_lifecycle=active
|
||||
onlyfun_version=0.0.0-alpha.23
|
||||
onlyfun_version=0.0.0-alpha.24
|
||||
onlyfun_lifecycle=active
|
||||
sanctuary_version=0.0.0-alpha.119
|
||||
sanctuary_lifecycle=active
|
||||
|
||||
@@ -28,3 +28,12 @@ explicitement d’Only Fun sans cycle inverse, enregistre le type d’entité so
|
||||
`abjection:shit_ball` et installe la fabrique commune au lancer manuel et au dispenser. Le
|
||||
projectile mesure 0,25 bloc, consomme une unité hors créatif, joue le son de boue à l’impact et
|
||||
ajoute sept secondes à la nausée déjà présente sans dupliquer d’objet.
|
||||
|
||||
## ABJ-03 — raccourci configurable
|
||||
|
||||
La catégorie de contrôles Only Fun expose « Chier », assigné à `J` par défaut. Le client utilise
|
||||
`consumeClick`, puis envoie un payload vide `onlyfun:c2s/shit_action`. Le serveur accepte au plus
|
||||
une demande par joueur et par tick, revalide que le joueur est toujours connecté, vivant et non
|
||||
spectateur, puis appelle le même `AbjectionActions.start(SHIT)` que `/shit`. Maintenir la touche
|
||||
ne produit donc pas de répétition par tick et la commande reste disponible. Only Fun conserve ses
|
||||
données v2 et passe son protocole réseau de 2 à 3.
|
||||
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
package fr.koka99cab.sanctuary26.onlyfun.abjection;
|
||||
|
||||
import fr.koka99cab.sanctuary26.onlyfun.api.abjection.AbjectionActions;
|
||||
import fr.koka99cab.sanctuary26.onlyfun.network.ShitActionRequestPayload;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents;
|
||||
import net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
|
||||
/** Validates connection/player state and deduplicates malicious same-tick key requests. */
|
||||
public final class ShitActionRequestHandler {
|
||||
private static final Map<UUID, Integer> LAST_REQUEST_TICK = new HashMap<>();
|
||||
|
||||
private ShitActionRequestHandler() {
|
||||
}
|
||||
|
||||
public static void register() {
|
||||
ServerPlayNetworking.registerGlobalReceiver(ShitActionRequestPayload.ID,
|
||||
(payload, context) -> handle(context.player()));
|
||||
ServerPlayConnectionEvents.DISCONNECT.register((handler, server) ->
|
||||
clear(handler.player.getUUID()));
|
||||
}
|
||||
|
||||
private static void handle(ServerPlayer player) {
|
||||
MinecraftServer server = player.level().getServer();
|
||||
if (server == null || server.getPlayerList().getPlayer(player.getUUID()) != player
|
||||
|| !player.isAlive() || player.isSpectator()
|
||||
|| !acceptTick(player.getUUID(), server.getTickCount())) return;
|
||||
AbjectionActions.start(player, AbjectionActions.Action.SHIT);
|
||||
}
|
||||
|
||||
static boolean acceptTick(UUID playerId, int tick) {
|
||||
Integer previous = LAST_REQUEST_TICK.put(playerId, tick);
|
||||
return previous == null || previous != tick;
|
||||
}
|
||||
|
||||
static void clear(UUID playerId) {
|
||||
LAST_REQUEST_TICK.remove(playerId);
|
||||
}
|
||||
}
|
||||
@@ -3,17 +3,34 @@ package fr.koka99cab.sanctuary26.onlyfun.client;
|
||||
import fr.koka99cab.sanctuary26.iliketomoveit.physics.VehicleFlightPhysics;
|
||||
import fr.koka99cab.sanctuary26.onlyfun.chickenboat.ChickenBoatControlPayload;
|
||||
import fr.koka99cab.sanctuary26.onlyfun.chickenboat.ChickenBoatUtil;
|
||||
import fr.koka99cab.sanctuary26.onlyfun.OnlyFunMod;
|
||||
import fr.koka99cab.sanctuary26.onlyfun.network.ShitActionRequestPayload;
|
||||
import com.mojang.blaze3d.platform.InputConstants;
|
||||
import net.fabricmc.api.ClientModInitializer;
|
||||
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
|
||||
import net.fabricmc.fabric.api.client.keymapping.v1.KeyMappingHelper;
|
||||
import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking;
|
||||
import net.minecraft.client.KeyMapping;
|
||||
import net.minecraft.world.entity.Entity;
|
||||
import net.minecraft.world.entity.vehicle.boat.AbstractBoat;
|
||||
import org.lwjgl.glfw.GLFW;
|
||||
|
||||
public final class OnlyFunClient implements ClientModInitializer {
|
||||
private static final KeyMapping.Category ONLY_FUN_CATEGORY =
|
||||
KeyMapping.Category.register(OnlyFunMod.id("onlyfun"));
|
||||
private static KeyMapping shitKey;
|
||||
|
||||
@Override
|
||||
public void onInitializeClient() {
|
||||
shitKey = KeyMappingHelper.registerKeyMapping(new KeyMapping(
|
||||
"key.onlyfun.shit", InputConstants.Type.KEYSYM, GLFW.GLFW_KEY_J, ONLY_FUN_CATEGORY));
|
||||
ClientTickEvents.END_CLIENT_TICK.register(client -> {
|
||||
if (client.player == null || client.level == null) return;
|
||||
while (shitKey.consumeClick()) {
|
||||
if (ClientPlayNetworking.canSend(ShitActionRequestPayload.ID)) {
|
||||
ClientPlayNetworking.send(new ShitActionRequestPayload());
|
||||
}
|
||||
}
|
||||
Entity vehicle = client.player.getVehicle();
|
||||
if (!(vehicle instanceof AbstractBoat boat) || !ChickenBoatUtil.hasChickenPassenger(boat)) return;
|
||||
if (!ClientPlayNetworking.canSend(ChickenBoatControlPayload.ID)) return;
|
||||
|
||||
+3
-1
@@ -9,7 +9,7 @@ import net.fabricmc.fabric.api.networking.v1.PayloadTypeRegistry;
|
||||
* Networking contract for Only Fun. Payload IDs must use the module namespace.
|
||||
*/
|
||||
public final class OnlyFunNetworking {
|
||||
public static final int PROTOCOL_VERSION = 2;
|
||||
public static final int PROTOCOL_VERSION = 3;
|
||||
private static boolean initialized;
|
||||
|
||||
private OnlyFunNetworking() {
|
||||
@@ -21,7 +21,9 @@ public final class OnlyFunNetworking {
|
||||
}
|
||||
initialized = true;
|
||||
PayloadTypeRegistry.serverboundPlay().register(ChickenBoatControlPayload.ID, ChickenBoatControlPayload.CODEC);
|
||||
PayloadTypeRegistry.serverboundPlay().register(ShitActionRequestPayload.ID, ShitActionRequestPayload.CODEC);
|
||||
ChickenBoatServerController.register();
|
||||
fr.koka99cab.sanctuary26.onlyfun.abjection.ShitActionRequestHandler.register();
|
||||
OnlyFunMod.LOGGER.debug("[{}] Networking contract v{} ready; chicken boat controls registered.",
|
||||
OnlyFunMod.DISPLAY_NAME, PROTOCOL_VERSION);
|
||||
}
|
||||
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
package fr.koka99cab.sanctuary26.onlyfun.network;
|
||||
|
||||
import fr.koka99cab.sanctuary26.onlyfun.OnlyFunMod;
|
||||
import net.minecraft.network.RegistryFriendlyByteBuf;
|
||||
import net.minecraft.network.codec.StreamCodec;
|
||||
import net.minecraft.network.protocol.common.custom.CustomPacketPayload;
|
||||
|
||||
/** Empty and therefore strictly bounded request to start the existing server-owned action. */
|
||||
public record ShitActionRequestPayload() implements CustomPacketPayload {
|
||||
public static final Type<ShitActionRequestPayload> ID =
|
||||
new Type<>(OnlyFunMod.id("c2s/shit_action"));
|
||||
public static final StreamCodec<RegistryFriendlyByteBuf, ShitActionRequestPayload> CODEC =
|
||||
CustomPacketPayload.codec((payload, buffer) -> {}, buffer -> new ShitActionRequestPayload());
|
||||
|
||||
@Override
|
||||
public Type<? extends CustomPacketPayload> type() {
|
||||
return ID;
|
||||
}
|
||||
}
|
||||
@@ -41,5 +41,7 @@
|
||||
"command.onlyfun.history": "Rolls: %s | dry streak: %s | anomalies: %s | last: %s",
|
||||
"command.onlyfun.give": "Granted Only Fun source: %s",
|
||||
"command.onlyfun.roll": "Outcome: %s (%s)",
|
||||
"command.onlyfun.simulate": "Simulation %s × %s: %s"
|
||||
"command.onlyfun.simulate": "Simulation %s × %s: %s",
|
||||
"key.categories.onlyfun.onlyfun": "Only Fun",
|
||||
"key.onlyfun.shit": "Shit"
|
||||
}
|
||||
|
||||
@@ -41,5 +41,7 @@
|
||||
"command.onlyfun.history": "Tirages : %s | série sèche : %s | anomalies : %s | dernier : %s",
|
||||
"command.onlyfun.give": "Source Only Fun donnée : %s",
|
||||
"command.onlyfun.roll": "Résultat : %s (%s)",
|
||||
"command.onlyfun.simulate": "Simulation %s × %s : %s"
|
||||
"command.onlyfun.simulate": "Simulation %s × %s : %s",
|
||||
"key.categories.onlyfun.onlyfun": "Only Fun",
|
||||
"key.onlyfun.shit": "Chier"
|
||||
}
|
||||
|
||||
@@ -41,5 +41,7 @@
|
||||
"command.onlyfun.history": "Розыгрыши: %s | серия: %s | аномалии: %s | последнее: %s",
|
||||
"command.onlyfun.give": "Выдан источник Only Fun: %s",
|
||||
"command.onlyfun.roll": "Результат: %s (%s)",
|
||||
"command.onlyfun.simulate": "Симуляция %s × %s: %s"
|
||||
"command.onlyfun.simulate": "Симуляция %s × %s: %s",
|
||||
"key.categories.onlyfun.onlyfun": "Only Fun",
|
||||
"key.onlyfun.shit": "Испражниться"
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
"system_type": "gameplay-system",
|
||||
"namespace": "onlyfun",
|
||||
"data_version": 2,
|
||||
"network_protocol": 2,
|
||||
"network_protocol": 3,
|
||||
"lifecycle": "${lifecycle}",
|
||||
"pack_version": "${pack_version}",
|
||||
"descriptions": {
|
||||
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
package fr.koka99cab.sanctuary26.onlyfun.abjection;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
/** Executable check for one physical press and same-tick spam deduplication. */
|
||||
public final class ShitActionRequestSmoke {
|
||||
private ShitActionRequestSmoke() {
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
UUID playerId = UUID.fromString("00000000-0000-0000-0000-000000000059");
|
||||
require(ShitActionRequestHandler.acceptTick(playerId, 100), "first press was rejected");
|
||||
require(!ShitActionRequestHandler.acceptTick(playerId, 100), "same-tick spam was accepted");
|
||||
require(ShitActionRequestHandler.acceptTick(playerId, 101), "next physical press was rejected");
|
||||
ShitActionRequestHandler.clear(playerId);
|
||||
require(ShitActionRequestHandler.acceptTick(playerId, 101), "disconnect cleanup retained input state");
|
||||
ShitActionRequestHandler.clear(playerId);
|
||||
}
|
||||
|
||||
private static void require(boolean condition, String message) {
|
||||
if (!condition) throw new AssertionError(message);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user