From fa10c5029fe4fbbeeca358a67db501a88123ec61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Nowak?= Date: Tue, 31 Dec 2024 11:31:44 +1100 Subject: [PATCH] SPIGOT-5784, SPIGOT-6858, #1527: Add villager reputation API --- .../world/entity/ai/gossip/Reputation.patch | 221 ++++++++++++++++++ .../entity/ai/village/ReputationEvent.patch | 29 +++ .../world/entity/npc/EntityVillager.patch | 74 +++++- .../craftbukkit/entity/CraftVillager.java | 154 ++++++++++++ .../craftbukkit/event/CraftEventFactory.java | 9 + .../craftbukkit/util/CraftMagicNumbers.java | 16 ++ .../bukkit/entity/ReputationEventTest.java | 52 +++++ .../org/bukkit/entity/ReputationTypeTest.java | 43 ++++ .../java/org/bukkit/entity/VillagerTest.java | 107 +++++++++ .../extension/AllFeaturesExtension.java | 4 + 10 files changed, 699 insertions(+), 10 deletions(-) create mode 100644 nms-patches/net/minecraft/world/entity/ai/gossip/Reputation.patch create mode 100644 nms-patches/net/minecraft/world/entity/ai/village/ReputationEvent.patch create mode 100644 src/test/java/org/bukkit/entity/ReputationEventTest.java create mode 100644 src/test/java/org/bukkit/entity/ReputationTypeTest.java create mode 100644 src/test/java/org/bukkit/entity/VillagerTest.java diff --git a/nms-patches/net/minecraft/world/entity/ai/gossip/Reputation.patch b/nms-patches/net/minecraft/world/entity/ai/gossip/Reputation.patch new file mode 100644 index 000000000..155e36332 --- /dev/null +++ b/nms-patches/net/minecraft/world/entity/ai/gossip/Reputation.patch @@ -0,0 +1,221 @@ +--- a/net/minecraft/world/entity/ai/gossip/Reputation.java ++++ b/net/minecraft/world/entity/ai/gossip/Reputation.java +@@ -30,13 +30,27 @@ + import net.minecraft.util.VisibleForDebug; + import org.slf4j.Logger; + ++// CraftBukkit start ++import net.minecraft.world.entity.npc.EntityVillager; ++import org.bukkit.craftbukkit.entity.CraftVillager.CraftReputationType; ++import org.bukkit.craftbukkit.event.CraftEventFactory; ++import org.bukkit.entity.Villager; ++import org.bukkit.event.entity.VillagerReputationChangeEvent; ++// CraftBukkit end ++ + public class Reputation { + + private static final Logger LOGGER = LogUtils.getLogger(); + public static final int DISCARD_THRESHOLD = 2; + private final Map gossips = Maps.newHashMap(); + +- public Reputation() {} ++ // CraftBukkit start - store reference to villager entity ++ private final EntityVillager villager; ++ ++ public Reputation(EntityVillager villager) { ++ this.villager = villager; ++ } ++ // CraftBukkit end + + @VisibleForDebug + public Map> getGossipEntries() { +@@ -51,15 +65,17 @@ + } + + public void decay() { +- Iterator iterator = this.gossips.values().iterator(); ++ Iterator> iterator = this.gossips.entrySet().iterator(); // CraftBukkit - iterate over entries instead of values to access entity UUID + + while (iterator.hasNext()) { +- Reputation.a reputation_a = (Reputation.a) iterator.next(); ++ // CraftBukkit start - pass villager and entity UUID to decay method ++ Map.Entry reputation_a = iterator.next(); + +- reputation_a.decay(); +- if (reputation_a.isEmpty()) { ++ reputation_a.getValue().decay(villager, reputation_a.getKey()); ++ if (reputation_a.getValue().isEmpty()) { + iterator.remove(); + } ++ // CraftBukkit end + } + + } +@@ -112,16 +128,27 @@ + int j = reputation_b.value - reputation_b.type.decayPerTransfer; + + if (j >= 2) { +- this.getOrCreate(reputation_b.target).entries.mergeInt(reputation_b.type, j, Reputation::mergeValuesForTransfer); ++ // CraftBukkit start - redirect to a method which fires an event before setting value ++ this.set(reputation_b.target, reputation_b.type, Reputation.mergeValuesForTransfer(getReputation(reputation_b.target, Predicate.isEqual(reputation_b.type), false), j), Villager.ReputationEvent.GOSSIP); ++ //this.getOrCreate(reputation_b.target).entries.mergeInt(reputation_b.type, j, Reputation::mergeValuesForTransfer); ++ // CraftBukkit end + } + + }); + } + + public int getReputation(UUID uuid, Predicate predicate) { ++ // CraftBukkit start - add getReputation overload with additional parameter ++ return getReputation(uuid, predicate, true); ++ } ++ ++ public int getReputation(UUID uuid, Predicate predicate, boolean weighted) { ++ // CraftBukkit end + Reputation.a reputation_a = (Reputation.a) this.gossips.get(uuid); + +- return reputation_a != null ? reputation_a.weightedValue(predicate) : 0; ++ // CraftBukkit start - handle weighted parameter ++ return reputation_a != null ? (weighted ? reputation_a.weightedValue(predicate) : reputation_a.unweightedValue(predicate)) : 0; ++ // CraftBukkit end + } + + public long getCountForType(ReputationType reputationtype, DoublePredicate doublepredicate) { +@@ -131,27 +158,58 @@ + } + + public void add(UUID uuid, ReputationType reputationtype, int i) { ++ // CraftBukkit start - add change reason parameter ++ add(uuid, reputationtype, i, Villager.ReputationEvent.UNSPECIFIED); ++ } ++ ++ public void add(UUID uuid, ReputationType reputationtype, int i, Villager.ReputationEvent changeReason) { ++ // CraftBukkit end + Reputation.a reputation_a = this.getOrCreate(uuid); + ++ int oldValue = reputation_a.entries.getInt(reputationtype); // CraftBukkit - store old value + reputation_a.entries.mergeInt(reputationtype, i, (j, k) -> { + return this.mergeValuesForAddition(reputationtype, j, k); + }); +- reputation_a.makeSureValueIsntTooLowOrTooHigh(reputationtype); ++ // CraftBukkit start - fire reputation change event ++ int newValue = reputation_a.entries.getInt(reputationtype); ++ newValue = Math.max(0, Math.min(newValue, reputationtype.max)); ++ reputation_a.entries.replace(reputationtype, oldValue); // restore old value until the event completed processing ++ VillagerReputationChangeEvent event = CraftEventFactory.callVillagerReputationChangeEvent((Villager) villager.getBukkitEntity(), uuid, changeReason, CraftReputationType.minecraftToBukkit(reputationtype), oldValue, newValue, reputationtype.max); ++ if (!event.isCancelled()) { ++ reputation_a.entries.replace(reputationtype, event.getNewValue()); ++ reputation_a.makeSureValueIsntTooLowOrTooHigh(reputationtype); ++ } ++ // CraftBukkit end + if (reputation_a.isEmpty()) { + this.gossips.remove(uuid); + } + + } + +- public void remove(UUID uuid, ReputationType reputationtype, int i) { +- this.add(uuid, reputationtype, -i); ++ // CraftBukkit start ++ public void set(UUID uuid, ReputationType reputationType, int i, Villager.ReputationEvent changeReason) { ++ int addAmount = i - getReputation(uuid, Predicate.isEqual(reputationType), false); ++ if (addAmount == 0) { ++ return; ++ } ++ this.add(uuid, reputationType, addAmount, changeReason); + } ++ // CraftBukkit end + +- public void remove(UUID uuid, ReputationType reputationtype) { ++ // CraftBukkit start - add change reason parameter ++ public void remove(UUID uuid, ReputationType reputationtype, int i, Villager.ReputationEvent changeReason) { ++ this.add(uuid, reputationtype, -i, changeReason); ++ } ++ // CraftBukkit end ++ ++ public void remove(UUID uuid, ReputationType reputationtype, Villager.ReputationEvent changeReason) { // CraftBukkit - add change reason parameter + Reputation.a reputation_a = (Reputation.a) this.gossips.get(uuid); + + if (reputation_a != null) { +- reputation_a.remove(reputationtype); ++ // CraftBukkit start - redirect - set to 0 instead ++ set(uuid, reputationtype, 0, changeReason); ++ //reputation_a.remove(reputationtype); ++ // CraftBukkit end + if (reputation_a.isEmpty()) { + this.gossips.remove(uuid); + } +@@ -159,7 +217,16 @@ + + } + +- public void remove(ReputationType reputationtype) { ++ public void remove(ReputationType reputationtype, Villager.ReputationEvent changeReason) { // CraftBukkit - add change reason parameter ++ // CraftBukkit start - replace the logic to call the other remove instead ++ Set uuids = Sets.newHashSet(this.gossips.keySet()); ++ for (UUID uuid : uuids) { ++ remove(uuid, reputationtype, changeReason); ++ } ++ if (true) { ++ return; ++ } ++ // CraftBukkit end + Iterator iterator = this.gossips.values().iterator(); + + while (iterator.hasNext()) { +@@ -174,7 +241,7 @@ + } + + public T store(DynamicOps dynamicops) { +- Optional optional = Reputation.b.LIST_CODEC.encodeStart(dynamicops, this.unpack().toList()).resultOrPartial((s) -> { ++ Optional optional = Reputation.b.LIST_CODEC.encodeStart(dynamicops, this.unpack().toList()).resultOrPartial((s) -> { // CraftBukkit - missing generic parameter after decompile + Reputation.LOGGER.warn("Failed to serialize gossips: {}", s); + }); + +@@ -186,7 +253,7 @@ + Reputation.b.LIST_CODEC.decode(dynamic).resultOrPartial((s) -> { + Reputation.LOGGER.warn("Failed to deserialize gossips: {}", s); + }).stream().flatMap((pair) -> { +- return ((List) pair.getFirst()).stream(); ++ return ((List) pair.getFirst()).stream(); // CraftBukkit - missing generic parameter after decompile + }).forEach((reputation_b) -> { + this.getOrCreate(reputation_b.target).entries.put(reputation_b.type, reputation_b.value); + }); +@@ -216,18 +283,36 @@ + }).sum(); + } + ++ // CraftBukkit start ++ public int unweightedValue(Predicate predicate) { ++ return this.entries.object2IntEntrySet().stream().filter((entry) -> { ++ return predicate.test((ReputationType) entry.getKey()); ++ }).mapToInt((entry) -> { ++ return entry.getIntValue(); ++ }).sum(); ++ } ++ // CraftBukkit end ++ + public Stream unpack(UUID uuid) { + return this.entries.object2IntEntrySet().stream().map((entry) -> { + return new Reputation.b(uuid, (ReputationType) entry.getKey(), entry.getIntValue()); + }); + } + +- public void decay() { ++ public void decay(EntityVillager villager, UUID uuid) { // CraftBukkit - add villager and entity uuid parameters + ObjectIterator> objectiterator = this.entries.object2IntEntrySet().iterator(); + + while (objectiterator.hasNext()) { + Entry entry = (Entry) objectiterator.next(); + int i = entry.getIntValue() - ((ReputationType) entry.getKey()).decayPerDay; ++ // CraftBukkit start - fire event ++ VillagerReputationChangeEvent event = CraftEventFactory.callVillagerReputationChangeEvent((Villager) villager.getBukkitEntity(), uuid, Villager.ReputationEvent.DECAY, CraftReputationType.minecraftToBukkit(entry.getKey()), entry.getIntValue(), i, entry.getKey().max); ++ if (event.isCancelled()) { ++ continue; ++ } else { ++ i = event.getNewValue(); ++ } ++ // CraftBukkit end + + if (i < 2) { + objectiterator.remove(); diff --git a/nms-patches/net/minecraft/world/entity/ai/village/ReputationEvent.patch b/nms-patches/net/minecraft/world/entity/ai/village/ReputationEvent.patch new file mode 100644 index 000000000..f9f73f661 --- /dev/null +++ b/nms-patches/net/minecraft/world/entity/ai/village/ReputationEvent.patch @@ -0,0 +1,29 @@ +--- a/net/minecraft/world/entity/ai/village/ReputationEvent.java ++++ b/net/minecraft/world/entity/ai/village/ReputationEvent.java +@@ -2,14 +2,26 @@ + + public interface ReputationEvent { + ++ java.util.Map BY_ID = com.google.common.collect.Maps.newHashMap(); // CraftBukkit - map with all values + ReputationEvent ZOMBIE_VILLAGER_CURED = register("zombie_villager_cured"); + ReputationEvent GOLEM_KILLED = register("golem_killed"); + ReputationEvent VILLAGER_HURT = register("villager_hurt"); + ReputationEvent VILLAGER_KILLED = register("villager_killed"); + ReputationEvent TRADE = register("trade"); ++ // CraftBukkit start - additional events added in the API ++ ReputationEvent GOSSIP = register("bukkit_gossip"); ++ ReputationEvent DECAY = register("bukkit_decay"); ++ ReputationEvent UNSPECIFIED = register("bukkit_unspecified"); ++ // CraftBukkit end + + static ReputationEvent register(final String s) { + return new ReputationEvent() { ++ // CraftBukkit start - add new value to map ++ { ++ BY_ID.put(s, this); ++ } ++ // CraftBukkit end ++ + public String toString() { + return s; + } diff --git a/nms-patches/net/minecraft/world/entity/npc/EntityVillager.patch b/nms-patches/net/minecraft/world/entity/npc/EntityVillager.patch index 8bb995856..88fdc69ad 100644 --- a/nms-patches/net/minecraft/world/entity/npc/EntityVillager.patch +++ b/nms-patches/net/minecraft/world/entity/npc/EntityVillager.patch @@ -16,7 +16,28 @@ public class EntityVillager extends EntityVillagerAbstract implements ReputationHandler, VillagerDataHolder { private static final Logger LOGGER = LogUtils.getLogger(); -@@ -150,7 +159,7 @@ +@@ -133,6 +142,9 @@ + }, MemoryModuleType.MEETING_POINT, (entityvillager, holder) -> { + return holder.is(PoiTypes.MEETING); + }); ++ // CraftBukkit start ++ public long gossipDecayInterval = GOSSIP_DECAY_INTERVAL; ++ // CraftBukkit end + + public EntityVillager(EntityTypes entitytypes, World world) { + this(entitytypes, world, VillagerType.PLAINS); +@@ -140,7 +152,9 @@ + + public EntityVillager(EntityTypes entitytypes, World world, VillagerType villagertype) { + super(entitytypes, world); +- this.gossips = new Reputation(); ++ // CraftBukkit start - add constructor parameter in Reputation ++ this.gossips = new Reputation(this); ++ // CraftBukkit end + ((Navigation) this.getNavigation()).setCanOpenDoors(true); + this.getNavigation().setCanFloat(true); + this.getNavigation().setRequiredPathLength(48.0F); +@@ -150,7 +164,7 @@ @Override public BehaviorController getBrain() { @@ -25,7 +46,7 @@ } @Override -@@ -235,7 +244,7 @@ +@@ -235,7 +249,7 @@ this.increaseProfessionLevelOnUpdate = false; } @@ -34,7 +55,7 @@ } } -@@ -360,7 +369,13 @@ +@@ -360,7 +374,13 @@ while (iterator.hasNext()) { MerchantRecipe merchantrecipe = (MerchantRecipe) iterator.next(); @@ -49,7 +70,7 @@ } this.resendOffersToTradingPlayer(); -@@ -429,7 +444,13 @@ +@@ -429,7 +449,13 @@ while (iterator.hasNext()) { MerchantRecipe merchantrecipe = (MerchantRecipe) iterator.next(); @@ -64,7 +85,7 @@ } } -@@ -489,7 +510,7 @@ +@@ -489,7 +515,7 @@ @Override public void addAdditionalSaveData(NBTTagCompound nbttagcompound) { super.addAdditionalSaveData(nbttagcompound); @@ -73,7 +94,7 @@ Logger logger = EntityVillager.LOGGER; Objects.requireNonNull(logger); -@@ -512,7 +533,7 @@ +@@ -512,7 +538,7 @@ public void readAdditionalSaveData(NBTTagCompound nbttagcompound) { super.readAdditionalSaveData(nbttagcompound); if (nbttagcompound.contains("VillagerData", 10)) { @@ -82,7 +103,7 @@ Logger logger = EntityVillager.LOGGER; Objects.requireNonNull(logger); -@@ -808,7 +829,7 @@ +@@ -808,7 +834,7 @@ entitywitch1.finalizeSpawn(worldserver, worldserver.getCurrentDifficultyAt(entitywitch1.blockPosition()), EntitySpawnReason.CONVERSION, (GroupDataEntity) null); entitywitch1.setPersistenceRequired(); this.releaseAllPois(); @@ -91,7 +112,16 @@ if (entitywitch == null) { super.thunderHit(worldserver, entitylightning); -@@ -906,7 +927,7 @@ +@@ -891,7 +917,7 @@ + + if (this.lastGossipDecayTime == 0L) { + this.lastGossipDecayTime = i; +- } else if (i >= this.lastGossipDecayTime + 24000L) { ++ } else if (i >= this.lastGossipDecayTime + gossipDecayInterval) { // CraftBukkit - use variable for decay interval + this.gossips.decay(); + this.lastGossipDecayTime = i; + } +@@ -906,7 +932,7 @@ }).limit(5L).toList(); if (list1.size() >= j) { @@ -100,7 +130,31 @@ list.forEach(SensorGolemLastSeen::golemDetected); } } -@@ -963,7 +984,7 @@ +@@ -919,15 +945,18 @@ + + @Override + public void onReputationEventFrom(ReputationEvent reputationevent, Entity entity) { ++ Villager.ReputationEvent bukkitReputationEvent = org.bukkit.craftbukkit.entity.CraftVillager.CraftReputationEvent.minecraftToBukkit(reputationevent); // CraftBukkit - convert event to bukkit + if (reputationevent == ReputationEvent.ZOMBIE_VILLAGER_CURED) { +- this.gossips.add(entity.getUUID(), ReputationType.MAJOR_POSITIVE, 20); +- this.gossips.add(entity.getUUID(), ReputationType.MINOR_POSITIVE, 25); ++ // CraftBukkit start - add change reason parameter ++ this.gossips.add(entity.getUUID(), ReputationType.MAJOR_POSITIVE, 20, bukkitReputationEvent); ++ this.gossips.add(entity.getUUID(), ReputationType.MINOR_POSITIVE, 25, bukkitReputationEvent); ++ // CraftBukkit end + } else if (reputationevent == ReputationEvent.TRADE) { +- this.gossips.add(entity.getUUID(), ReputationType.TRADING, 2); ++ this.gossips.add(entity.getUUID(), ReputationType.TRADING, 2, bukkitReputationEvent); // CraftBukkit - add change reason parameter + } else if (reputationevent == ReputationEvent.VILLAGER_HURT) { +- this.gossips.add(entity.getUUID(), ReputationType.MINOR_NEGATIVE, 25); ++ this.gossips.add(entity.getUUID(), ReputationType.MINOR_NEGATIVE, 25, bukkitReputationEvent); // CraftBukkit - add change reason parameter + } else if (reputationevent == ReputationEvent.VILLAGER_KILLED) { +- this.gossips.add(entity.getUUID(), ReputationType.MAJOR_NEGATIVE, 25); ++ this.gossips.add(entity.getUUID(), ReputationType.MAJOR_NEGATIVE, 25, bukkitReputationEvent); // CraftBukkit - add change reason parameter + } + + } +@@ -963,7 +992,7 @@ @Override public void startSleeping(BlockPosition blockposition) { super.startSleeping(blockposition); @@ -109,7 +163,7 @@ this.brain.eraseMemory(MemoryModuleType.WALK_TARGET); this.brain.eraseMemory(MemoryModuleType.CANT_REACH_WALK_TARGET_SINCE); } -@@ -971,7 +992,7 @@ +@@ -971,7 +1000,7 @@ @Override public void stopSleeping() { super.stopSleeping(); diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftVillager.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftVillager.java index 2c5e0b894..026abc250 100644 --- a/src/main/java/org/bukkit/craftbukkit/entity/CraftVillager.java +++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftVillager.java @@ -1,9 +1,16 @@ package org.bukkit.craftbukkit.entity; import com.google.common.base.Preconditions; +import com.google.common.collect.Maps; import java.util.Locale; +import java.util.Map; +import java.util.UUID; +import java.util.function.Predicate; +import java.util.stream.Collectors; +import java.util.stream.Stream; import net.minecraft.core.BlockPosition; import net.minecraft.core.registries.Registries; +import net.minecraft.world.entity.ai.gossip.ReputationType; import net.minecraft.world.entity.monster.EntityZombie; import net.minecraft.world.entity.monster.EntityZombieVillager; import net.minecraft.world.entity.npc.EntityVillager; @@ -128,6 +135,79 @@ public class CraftVillager extends CraftAbstractVillager implements Villager { return (entityzombievillager != null) ? (ZombieVillager) entityzombievillager.getBukkitEntity() : null; } + @Override + public int getReputation(UUID uuid, ReputationType reputationType) { + Preconditions.checkArgument(uuid != null, "UUID cannot be null"); + Preconditions.checkArgument(reputationType != null, "Reputation type cannot be null"); + return getHandle().getGossips().getReputation(uuid, + Predicate.isEqual(CraftReputationType.bukkitToMinecraft(reputationType)), + false); + } + + @Override + public int getWeightedReputation(UUID uuid, ReputationType reputationType) { + Preconditions.checkArgument(uuid != null, "UUID cannot be null"); + Preconditions.checkArgument(reputationType != null, "Reputation type cannot be null"); + return getHandle().getGossips().getReputation(uuid, + Predicate.isEqual(CraftReputationType.bukkitToMinecraft(reputationType)), + true); + } + + @Override + public int getReputation(UUID uuid) { + Preconditions.checkArgument(uuid != null, "UUID cannot be null"); + return getHandle().getGossips().getReputation(uuid, reputationType -> true); + } + + @Override + public void addReputation(UUID uuid, ReputationType reputationType, int amount) { + addReputation(uuid, reputationType, amount, ReputationEvent.UNSPECIFIED); + } + + @Override + public void addReputation(UUID uuid, ReputationType reputationType, int amount, ReputationEvent changeReason) { + Preconditions.checkArgument(uuid != null, "UUID cannot be null"); + Preconditions.checkArgument(reputationType != null, "Reputation type cannot be null"); + Preconditions.checkArgument(changeReason != null, "Change reason cannot be null"); + getHandle().getGossips().add(uuid, CraftReputationType.bukkitToMinecraft(reputationType), amount, changeReason); + } + + @Override + public void removeReputation(UUID uuid, ReputationType reputationType, int amount) { + removeReputation(uuid, reputationType, amount, ReputationEvent.UNSPECIFIED); + } + + @Override + public void removeReputation(UUID uuid, ReputationType reputationType, int amount, ReputationEvent changeReason) { + Preconditions.checkArgument(uuid != null, "UUID cannot be null"); + Preconditions.checkArgument(reputationType != null, "Reputation type cannot be null"); + Preconditions.checkArgument(changeReason != null, "Change reason cannot be null"); + getHandle().getGossips().remove(uuid, CraftReputationType.bukkitToMinecraft(reputationType), amount, changeReason); + } + + @Override + public void setReputation(UUID uuid, ReputationType reputationType, int amount) { + setReputation(uuid, reputationType, amount, ReputationEvent.UNSPECIFIED); + } + + @Override + public void setReputation(UUID uuid, ReputationType reputationType, int amount, ReputationEvent changeReason) { + Preconditions.checkArgument(uuid != null, "UUID cannot be null"); + Preconditions.checkArgument(reputationType != null, "Reputation type cannot be null"); + Preconditions.checkArgument(changeReason != null, "Change reason cannot be null"); + getHandle().getGossips().set(uuid, CraftReputationType.bukkitToMinecraft(reputationType), amount, changeReason); + } + + @Override + public void setGossipDecayTime(long ticks) { + getHandle().gossipDecayInterval = ticks; + } + + @Override + public long getGossipDecayTime() { + return getHandle().gossipDecayInterval; + } + public static class CraftType implements Type, Handleable { private static int count = 0; @@ -289,4 +369,78 @@ public class CraftVillager extends CraftAbstractVillager implements Villager { return getKey().hashCode(); } } + + public static class CraftReputationType implements ReputationType, Handleable { + + public static final Map BY_ID = Stream + .of(net.minecraft.world.entity.ai.gossip.ReputationType.values()) + .collect(Collectors.toMap(reputationType -> reputationType.id, CraftReputationType::new)); + private final net.minecraft.world.entity.ai.gossip.ReputationType handle; + + public CraftReputationType(net.minecraft.world.entity.ai.gossip.ReputationType handle) { + this.handle = handle; + } + + @Override + public net.minecraft.world.entity.ai.gossip.ReputationType getHandle() { + return handle; + } + + @Override + public int getMaxValue() { + return handle.max; + } + + @Override + public int getWeight() { + return handle.weight; + } + + public static net.minecraft.world.entity.ai.gossip.ReputationType bukkitToMinecraft(ReputationType bukkit) { + Preconditions.checkArgument(bukkit != null); + + return ((CraftReputationType) bukkit).getHandle(); + } + + public static ReputationType minecraftToBukkit(net.minecraft.world.entity.ai.gossip.ReputationType minecraft) { + Preconditions.checkArgument(minecraft != null); + + return switch (minecraft) { + case MAJOR_NEGATIVE -> ReputationType.MAJOR_NEGATIVE; + case MINOR_NEGATIVE -> ReputationType.MINOR_NEGATIVE; + case MINOR_POSITIVE -> ReputationType.MINOR_POSITIVE; + case MAJOR_POSITIVE -> ReputationType.MAJOR_POSITIVE; + case TRADING -> ReputationType.TRADING; + }; + } + } + + public static class CraftReputationEvent implements ReputationEvent, Handleable { + + private static final Map ALL = Maps.newHashMap(); + private final net.minecraft.world.entity.ai.village.ReputationEvent handle; + + public CraftReputationEvent(net.minecraft.world.entity.ai.village.ReputationEvent handle) { + this.handle = handle; + ALL.put(handle.toString(), this); + } + + @Override + public net.minecraft.world.entity.ai.village.ReputationEvent getHandle() { + return handle; + } + + public static net.minecraft.world.entity.ai.village.ReputationEvent bukkitToMinecraft(ReputationEvent bukkit) { + Preconditions.checkArgument(bukkit != null); + + return ((CraftReputationEvent) bukkit).getHandle(); + } + + public static ReputationEvent minecraftToBukkit(net.minecraft.world.entity.ai.village.ReputationEvent minecraft) { + Preconditions.checkArgument(minecraft != null); + + ReputationEvent bukkit = ALL.get(minecraft.toString()); + return bukkit == null ? new CraftReputationEvent(minecraft) : bukkit; + } + } } diff --git a/src/main/java/org/bukkit/craftbukkit/event/CraftEventFactory.java b/src/main/java/org/bukkit/craftbukkit/event/CraftEventFactory.java index 4858e0063..9ef6ac886 100644 --- a/src/main/java/org/bukkit/craftbukkit/event/CraftEventFactory.java +++ b/src/main/java/org/bukkit/craftbukkit/event/CraftEventFactory.java @@ -12,6 +12,7 @@ import java.util.Collections; import java.util.EnumMap; import java.util.List; import java.util.Map; +import java.util.UUID; import java.util.stream.Collectors; import java.util.stream.Stream; import javax.annotation.Nullable; @@ -220,6 +221,7 @@ import org.bukkit.event.entity.SpawnerSpawnEvent; import org.bukkit.event.entity.StriderTemperatureChangeEvent; import org.bukkit.event.entity.TrialSpawnerSpawnEvent; import org.bukkit.event.entity.VillagerCareerChangeEvent; +import org.bukkit.event.entity.VillagerReputationChangeEvent; import org.bukkit.event.inventory.InventoryCloseEvent; import org.bukkit.event.inventory.InventoryOpenEvent; import org.bukkit.event.inventory.PrepareAnvilEvent; @@ -1928,4 +1930,11 @@ public class CraftEventFactory { Bukkit.getPluginManager().callEvent(new EntityRemoveEvent(entity.getBukkitEntity(), cause)); } + + public static VillagerReputationChangeEvent callVillagerReputationChangeEvent(Villager villager, UUID targetUuid, Villager.ReputationEvent reason, Villager.ReputationType reputationType, int oldValue, int newValue, int maxValue) { + VillagerReputationChangeEvent event = new VillagerReputationChangeEvent(villager, targetUuid, reason, reputationType, oldValue, newValue, maxValue); + + Bukkit.getPluginManager().callEvent(event); + return event; + } } diff --git a/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java b/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java index 71cd9b42d..62d8ba6b2 100644 --- a/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java +++ b/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java @@ -16,6 +16,7 @@ import java.io.IOException; import java.util.HashMap; import java.util.Locale; import java.util.Map; +import java.util.Optional; import java.util.logging.Level; import java.util.logging.Logger; import net.minecraft.SharedConstants; @@ -32,6 +33,7 @@ import net.minecraft.server.MinecraftServer; import net.minecraft.util.datafix.DataConverterRegistry; import net.minecraft.util.datafix.fixes.DataConverterTypes; import net.minecraft.world.entity.EntityTypes; +import net.minecraft.world.entity.ai.village.ReputationEvent; import net.minecraft.world.item.Item; import net.minecraft.world.item.alchemy.PotionRegistry; import net.minecraft.world.level.block.Block; @@ -56,6 +58,7 @@ import org.bukkit.craftbukkit.block.CraftBiome; import org.bukkit.craftbukkit.block.data.CraftBlockData; import org.bukkit.craftbukkit.damage.CraftDamageEffect; import org.bukkit.craftbukkit.damage.CraftDamageSourceBuilder; +import org.bukkit.craftbukkit.entity.CraftVillager; import org.bukkit.craftbukkit.inventory.CraftItemStack; import org.bukkit.craftbukkit.legacy.CraftLegacy; import org.bukkit.craftbukkit.legacy.FieldRename; @@ -65,6 +68,7 @@ import org.bukkit.damage.DamageSource; import org.bukkit.damage.DamageType; import org.bukkit.enchantments.Enchantment; import org.bukkit.entity.EntityType; +import org.bukkit.entity.Villager; import org.bukkit.inventory.CreativeCategory; import org.bukkit.inventory.EquipmentSlot; import org.bukkit.inventory.ItemStack; @@ -422,6 +426,18 @@ public final class CraftMagicNumbers implements UnsafeValues { return customBiome; } + @Override + public Villager.ReputationType createReputationType(String key) { + return Optional.ofNullable(CraftVillager.CraftReputationType.BY_ID.get(key)) + .orElseThrow(() -> new IllegalArgumentException("Invalid ReputationType key: " + key)); + } + + @Override + public Villager.ReputationEvent createReputationEvent(String key) { + return Optional.ofNullable(ReputationEvent.BY_ID.get(key)).map(CraftVillager.CraftReputationEvent::new) + .orElseThrow(() -> new IllegalArgumentException("Invalid ReputationEvent key: " + key)); + } + /** * This helper class represents the different NBT Tags. *

diff --git a/src/test/java/org/bukkit/entity/ReputationEventTest.java b/src/test/java/org/bukkit/entity/ReputationEventTest.java new file mode 100644 index 000000000..9894ec5ed --- /dev/null +++ b/src/test/java/org/bukkit/entity/ReputationEventTest.java @@ -0,0 +1,52 @@ +package org.bukkit.entity; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.List; +import net.minecraft.world.entity.ai.village.ReputationEvent; +import org.bukkit.craftbukkit.entity.CraftVillager; +import org.bukkit.support.environment.Normal; +import org.junit.jupiter.api.Test; + +@Normal +public class ReputationEventTest { + + @Test + public void toBukkit() throws IllegalAccessException { + List reputationEvents = getConstants(ReputationEvent.class); + List bukkitReputationEvents = getConstants(Villager.ReputationEvent.class); + for (ReputationEvent reputationEvent : reputationEvents) { + Villager.ReputationEvent bukkit = CraftVillager.CraftReputationEvent.minecraftToBukkit(reputationEvent); + assertNotNull(bukkit, "Reputation event " + reputationEvent.toString() + " should have a Bukkit equivalent"); + assertTrue(bukkitReputationEvents.contains(bukkit), "Reputation event " + reputationEvent.toString() + " should have a Bukkit equivalent"); + } + } + + @Test + public void toMinecraft() throws IllegalAccessException { + List reputationEvents = getConstants(ReputationEvent.class); + List bukkitReputationEvents = getConstants(Villager.ReputationEvent.class); + for (Villager.ReputationEvent reputationEvent : bukkitReputationEvents) { + ReputationEvent minecraft = CraftVillager.CraftReputationEvent.bukkitToMinecraft(reputationEvent); + assertNotNull(minecraft, "Reputation event " + reputationEvent.toString() + " should have a Minecraft equivalent"); + assertTrue(reputationEvents.contains(minecraft), "Reputation event " + reputationEvent + " should have a Minecraft equivalent"); + } + } + + private static boolean isPublicStaticFinal(Field field) { + return Modifier.isStatic(field.getModifiers()) && Modifier.isPublic(field.getModifiers()) && Modifier.isFinal(field.getModifiers()); + } + + private static List getConstants(Class clazz) throws IllegalAccessException { + List list = new ArrayList<>(); + for (Field field : clazz.getFields()) { + if (isPublicStaticFinal(field) && clazz.isAssignableFrom(field.getType())) { + list.add((T) field.get(null)); + } + } + return list; + } +} diff --git a/src/test/java/org/bukkit/entity/ReputationTypeTest.java b/src/test/java/org/bukkit/entity/ReputationTypeTest.java new file mode 100644 index 000000000..bf5d9e595 --- /dev/null +++ b/src/test/java/org/bukkit/entity/ReputationTypeTest.java @@ -0,0 +1,43 @@ +package org.bukkit.entity; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.List; +import net.minecraft.world.entity.ai.gossip.ReputationType; +import org.bukkit.craftbukkit.entity.CraftVillager; +import org.bukkit.support.environment.Normal; +import org.junit.jupiter.api.Test; + +@Normal +public class ReputationTypeTest { + + @Test + public void toBukkit() { + for (ReputationType reputationType : ReputationType.values()) { + assertNotNull(CraftVillager.CraftReputationType.minecraftToBukkit(reputationType), "ReputationType." + reputationType.name() + ".toBukkit() should not be null"); + } + } + + @Test + public void fromBukkit() throws IllegalAccessException { + for (Villager.ReputationType reputationType : getConstants(Villager.ReputationType.class)) { + assertNotNull(CraftVillager.CraftReputationType.bukkitToMinecraft(reputationType), "ReputationType.fromBukkit(Villager.ReputationType) should not be null"); + } + } + + private static boolean isPublicStaticFinal(Field field) { + return Modifier.isStatic(field.getModifiers()) && Modifier.isPublic(field.getModifiers()) && Modifier.isFinal(field.getModifiers()); + } + + private static List getConstants(Class clazz) throws IllegalAccessException { + List list = new ArrayList<>(); + for (Field field : clazz.getFields()) { + if (isPublicStaticFinal(field) && clazz.isAssignableFrom(field.getType())) { + list.add((T) field.get(null)); + } + } + return list; + } +} diff --git a/src/test/java/org/bukkit/entity/VillagerTest.java b/src/test/java/org/bukkit/entity/VillagerTest.java new file mode 100644 index 000000000..28b6ac90d --- /dev/null +++ b/src/test/java/org/bukkit/entity/VillagerTest.java @@ -0,0 +1,107 @@ +package org.bukkit.entity; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.*; +import java.util.UUID; +import net.minecraft.server.level.WorldServer; +import net.minecraft.world.entity.ai.gossip.Reputation; +import net.minecraft.world.entity.ai.gossip.ReputationType; +import net.minecraft.world.entity.npc.EntityVillager; +import net.minecraft.world.flag.FeatureFlags; +import org.bukkit.Location; +import org.bukkit.World; +import org.bukkit.craftbukkit.entity.CraftEntityTypes; +import org.bukkit.support.environment.AllFeatures; +import org.junit.jupiter.api.Test; + +@AllFeatures +public class VillagerTest { + + @Test + public void getReputation() { + Villager villager = createVillager(); + UUID uuid = UUID.randomUUID(); + villager.setReputation(uuid, Villager.ReputationType.MINOR_POSITIVE, 20); + villager.setReputation(uuid, Villager.ReputationType.MINOR_NEGATIVE, 10); + assertEquals(20, villager.getReputation(uuid, Villager.ReputationType.MINOR_POSITIVE), "getReputation should return correct value for a single reputation type"); + assertEquals(10, villager.getReputation(uuid, Villager.ReputationType.MINOR_NEGATIVE), "getReputation should return correct value for a single reputation type"); + assertEquals(10, villager.getReputation(uuid), "getReputation should return correct value for total weighted reputation"); + } + + @Test + public void getWeightedReputation() { + Villager villager = createVillager(); + UUID uuid = UUID.randomUUID(); + villager.setReputation(uuid, Villager.ReputationType.MINOR_POSITIVE, 20); + villager.setReputation(uuid, Villager.ReputationType.MINOR_NEGATIVE, 10); + assertEquals(20, villager.getWeightedReputation(uuid, Villager.ReputationType.MINOR_POSITIVE), "getWeightedReputation should return correct value for a single reputation type"); + assertEquals(-10, villager.getWeightedReputation(uuid, Villager.ReputationType.MINOR_NEGATIVE), "getWeightedReputation should return correct value for a single reputation type"); + } + + @Test + public void addReputation() { + Villager villager = createVillager(); + UUID uuid = UUID.randomUUID(); + villager.setReputation(uuid, Villager.ReputationType.MINOR_POSITIVE, 20); + villager.setReputation(uuid, Villager.ReputationType.MINOR_NEGATIVE, 10); + villager.addReputation(uuid, Villager.ReputationType.MINOR_POSITIVE, 3); + villager.addReputation(uuid, Villager.ReputationType.MINOR_NEGATIVE, 20); + assertEquals(23, villager.getReputation(uuid, Villager.ReputationType.MINOR_POSITIVE), "addReputation should increase value by given amount"); + assertEquals(30, villager.getReputation(uuid, Villager.ReputationType.MINOR_NEGATIVE), "addReputation should increase value by given amount"); + + villager.addReputation(uuid, Villager.ReputationType.MINOR_POSITIVE, ReputationType.MINOR_POSITIVE.max); + villager.addReputation(uuid, Villager.ReputationType.MINOR_NEGATIVE, ReputationType.MINOR_NEGATIVE.max); + assertEquals(ReputationType.MINOR_POSITIVE.max, villager.getReputation(uuid, Villager.ReputationType.MINOR_POSITIVE), "addReputation should not exceed maximum value"); + assertEquals(ReputationType.MINOR_NEGATIVE.max, villager.getReputation(uuid, Villager.ReputationType.MINOR_NEGATIVE), "addReputation should not exceed maximum value"); + } + + @Test + public void removeReputation() { + Villager villager = createVillager(); + UUID uuid = UUID.randomUUID(); + villager.setReputation(uuid, Villager.ReputationType.MINOR_POSITIVE, 20); + villager.setReputation(uuid, Villager.ReputationType.MINOR_NEGATIVE, 10); + villager.removeReputation(uuid, Villager.ReputationType.MINOR_POSITIVE, 5); + villager.removeReputation(uuid, Villager.ReputationType.MINOR_NEGATIVE, 2); + assertEquals(15, villager.getReputation(uuid, Villager.ReputationType.MINOR_POSITIVE), "removeReputation should decrease value by given amount"); + assertEquals(8, villager.getReputation(uuid, Villager.ReputationType.MINOR_NEGATIVE), "removeReputation should decrease value by given amount"); + + villager.removeReputation(uuid, Villager.ReputationType.MINOR_POSITIVE, 15 - Reputation.DISCARD_THRESHOLD + 1); + villager.removeReputation(uuid, Villager.ReputationType.MINOR_NEGATIVE, Integer.MAX_VALUE); + assertEquals(0, villager.getReputation(uuid, Villager.ReputationType.MINOR_POSITIVE), "removeReputation should cause reputation removal if value drops below discard threshold"); + assertEquals(0, villager.getReputation(uuid, Villager.ReputationType.MINOR_NEGATIVE), "removeReputation should cause reputation removal if value drops below discard threshold"); + } + + @Test + public void setReputation() { + Villager villager = createVillager(); + UUID uuid = UUID.randomUUID(); + villager.setReputation(uuid, Villager.ReputationType.MINOR_POSITIVE, 20); + villager.setReputation(uuid, Villager.ReputationType.MINOR_NEGATIVE, 10); + villager.setReputation(uuid, Villager.ReputationType.MINOR_POSITIVE, 5); + villager.setReputation(uuid, Villager.ReputationType.MINOR_NEGATIVE, 2); + assertEquals(5, villager.getReputation(uuid, Villager.ReputationType.MINOR_POSITIVE), "setReputation should set value to given amount"); + assertEquals(2, villager.getReputation(uuid, Villager.ReputationType.MINOR_NEGATIVE), "setReputation should set value to given amount"); + + villager.setReputation(uuid, Villager.ReputationType.MINOR_POSITIVE, Reputation.DISCARD_THRESHOLD - 1); + villager.setReputation(uuid, Villager.ReputationType.MINOR_NEGATIVE, Integer.MIN_VALUE); + assertEquals(0, villager.getReputation(uuid, Villager.ReputationType.MINOR_POSITIVE), "setReputation should cause reputation removal if value drops below discard threshold"); + assertEquals(0, villager.getReputation(uuid, Villager.ReputationType.MINOR_NEGATIVE), "setReputation should cause reputation removal if value drops below discard threshold"); + + villager.setReputation(uuid, Villager.ReputationType.MINOR_POSITIVE, ReputationType.MINOR_POSITIVE.max + 1); + villager.setReputation(uuid, Villager.ReputationType.MINOR_NEGATIVE, Integer.MAX_VALUE); + assertEquals(ReputationType.MINOR_POSITIVE.max, villager.getReputation(uuid, Villager.ReputationType.MINOR_POSITIVE), "setReputation should be clamped to reputation type maximum value"); + assertEquals(ReputationType.MINOR_NEGATIVE.max, villager.getReputation(uuid, Villager.ReputationType.MINOR_NEGATIVE), "setReputation should be clamped to reputation type maximum value"); + } + + private static Villager createVillager() { + World world = mock(withSettings().stubOnly()); + WorldServer worldServer = mock(withSettings().stubOnly()); + when(worldServer.getMinecraftWorld()).thenReturn(worldServer); + when(worldServer.enabledFeatures()).thenReturn(FeatureFlags.VANILLA_SET); + Location location = new Location(world, 0, 0, 0, 0, 0); + CraftEntityTypes.SpawnData spawnData = new CraftEntityTypes.SpawnData(worldServer, location, false, false); + EntityVillager entityVillager = (EntityVillager) CraftEntityTypes.getEntityTypeData(EntityType.VILLAGER).spawnFunction().apply(spawnData); + return (Villager) entityVillager.getBukkitEntity(); + } +} diff --git a/src/test/java/org/bukkit/support/extension/AllFeaturesExtension.java b/src/test/java/org/bukkit/support/extension/AllFeaturesExtension.java index f759b6c97..b5a81da36 100644 --- a/src/test/java/org/bukkit/support/extension/AllFeaturesExtension.java +++ b/src/test/java/org/bukkit/support/extension/AllFeaturesExtension.java @@ -10,6 +10,7 @@ import org.bukkit.Keyed; import org.bukkit.Registry; import org.bukkit.Server; import org.bukkit.craftbukkit.CraftRegistry; +import org.bukkit.plugin.PluginManager; import org.bukkit.support.DummyServerHelper; import org.bukkit.support.RegistryHelper; import org.junit.jupiter.api.extension.ExtensionContext; @@ -56,6 +57,9 @@ public class AllFeaturesExtension extends BaseExtension { return spy; }); + PluginManager pluginManager = mock(withSettings().stubOnly()); + when(server.getPluginManager()).thenReturn(pluginManager); + CraftRegistry.setMinecraftRegistry(RegistryHelper.getRegistry()); }