Support trade rebalance experiment ()

* Support trade rebalance experiment

* Add pool IDs

* Delayed pool modification

* Fix unused import
This commit is contained in:
apple502j 2023-09-22 03:16:10 +09:00 committed by GitHub
parent 0708114127
commit 219ee513db
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 294 additions and 6 deletions
fabric-object-builder-api-v1/src
main
java/net/fabricmc/fabric
api/object/builder/v1/trade
impl/object/builder
resources
testmod/java/net/fabricmc/fabric/test/object/builder

View file

@ -16,9 +16,13 @@
package net.fabricmc.fabric.api.object.builder.v1.trade;
import java.util.Collection;
import java.util.List;
import java.util.function.Consumer;
import org.jetbrains.annotations.ApiStatus;
import net.minecraft.util.Identifier;
import net.minecraft.village.TradeOffers;
import net.minecraft.village.VillagerProfession;
@ -30,6 +34,9 @@ import net.fabricmc.fabric.impl.object.builder.TradeOfferInternals;
public final class TradeOfferHelper {
/**
* Registers trade offer factories for use by villagers.
* This adds the same trade offers to current and rebalanced trades.
* To add separate offers for the rebalanced trade experiment, use
* {@link #registerVillagerOffers(VillagerProfession, int, VillagerOffersAdder)}.
*
* <p>Below is an example, of registering a trade offer factory to be added a blacksmith with a profession level of 3:
* <blockquote><pre>
@ -43,11 +50,38 @@ public final class TradeOfferHelper {
* @param factories a consumer to provide the factories
*/
public static void registerVillagerOffers(VillagerProfession profession, int level, Consumer<List<TradeOffers.Factory>> factories) {
TradeOfferInternals.registerVillagerOffers(profession, level, (trades, rebalanced) -> factories.accept(trades));
}
/**
* Registers trade offer factories for use by villagers.
* This method allows separate offers to be added depending on whether the rebalanced
* trade experiment is enabled.
* If a particular profession's rebalanced trade offers are not added at all, it falls back
* to the regular trade offers.
*
* <p>Below is an example, of registering a trade offer factory to be added a blacksmith with a profession level of 3:
* <blockquote><pre>
* TradeOfferHelper.registerVillagerOffers(VillagerProfession.BLACKSMITH, 3, (factories, rebalanced) -> {
* factories.add(new CustomTradeFactory(...);
* });
* </pre></blockquote>
*
* <p><strong>Experimental feature</strong>. This API may receive changes as necessary to adapt to further experiment changes.
*
* @param profession the villager profession to assign the trades to
* @param level the profession level the villager must be to offer the trades
* @param factories a consumer to provide the factories
*/
@ApiStatus.Experimental
public static void registerVillagerOffers(VillagerProfession profession, int level, VillagerOffersAdder factories) {
TradeOfferInternals.registerVillagerOffers(profession, level, factories);
}
/**
* Registers trade offer factories for use by wandering trades.
* This does not add offers for the rebalanced trade experiment.
* To add rebalanced trades, use {@link #registerRebalancedWanderingTraderOffers}.
*
* @param level the level the trades
* @param factory a consumer to provide the factories
@ -56,6 +90,20 @@ public final class TradeOfferHelper {
TradeOfferInternals.registerWanderingTraderOffers(level, factory);
}
/**
* Registers trade offer factories for use by wandering trades.
* This only adds offers for the rebalanced trade experiment.
* To add regular trades, use {@link #registerWanderingTraderOffers(int, Consumer)}.
*
* <p><strong>Experimental feature</strong>. This API may receive changes as necessary to adapt to further experiment changes.
*
* @param factory a consumer to add trade offers
*/
@ApiStatus.Experimental
public static synchronized void registerRebalancedWanderingTraderOffers(Consumer<WanderingTraderOffersBuilder> factory) {
factory.accept(new TradeOfferInternals.WanderingTraderOffersBuilderImpl());
}
/**
* @deprecated This never did anything useful.
*/
@ -66,4 +114,115 @@ public final class TradeOfferHelper {
private TradeOfferHelper() {
}
@FunctionalInterface
public interface VillagerOffersAdder {
void onRegister(List<TradeOffers.Factory> factories, boolean rebalanced);
}
/**
* A builder for rebalanced wandering trader offers.
*
* <p><strong>Experimental feature</strong>. This API may receive changes as necessary to adapt to further experiment changes.
*
* @see #registerRebalancedWanderingTraderOffers(Consumer)
*/
@ApiStatus.NonExtendable
@ApiStatus.Experimental
public interface WanderingTraderOffersBuilder {
/**
* The pool ID for the "buy items" pool.
* Two trade offers are picked from this pool.
*
* <p>In vanilla, this pool contains offers to buy water buckets, baked potatoes, etc.
* for emeralds.
*/
Identifier BUY_ITEMS_POOL = new Identifier("minecraft", "buy_items");
/**
* The pool ID for the "sell special items" pool.
* Two trade offers are picked from this pool.
*
* <p>In vanilla, this pool contains offers to sell logs, enchanted iron pickaxes, etc.
*/
Identifier SELL_SPECIAL_ITEMS_POOL = new Identifier("minecraft", "sell_special_items");
/**
* The pool ID for the "sell common items" pool.
* Five trade offers are picked from this pool.
*
* <p>In vanilla, this pool contains offers to sell flowers, saplings, etc.
*/
Identifier SELL_COMMON_ITEMS_POOL = new Identifier("minecraft", "sell_common_items");
/**
* Adds a new pool to the offer list. Exactly {@code count} offers are picked from
* {@code factories} and offered to customers.
* @param id the ID to be assigned to this pool, to allow further modification
* @param count the number of offers to be picked from {@code factories}
* @param factories the trade offer factories
* @return this builder, for chaining
* @throws IllegalArgumentException if {@code count} is not positive or if {@code factories} is empty
*/
WanderingTraderOffersBuilder pool(Identifier id, int count, TradeOffers.Factory... factories);
/**
* Adds a new pool to the offer list. Exactly {@code count} offers are picked from
* {@code factories} and offered to customers.
* @param id the ID to be assigned to this pool, to allow further modification
* @param count the number of offers to be picked from {@code factories}
* @param factories the trade offer factories
* @return this builder, for chaining
* @throws IllegalArgumentException if {@code count} is not positive or if {@code factories} is empty
*/
default WanderingTraderOffersBuilder pool(Identifier id, int count, Collection<? extends TradeOffers.Factory> factories) {
return pool(id, count, factories.toArray(TradeOffers.Factory[]::new));
}
/**
* Adds trade offers to the offer list. All offers from {@code factories} are
* offered to each customer.
* @param id the ID to be assigned to this pool, to allow further modification
* @param factories the trade offer factories
* @return this builder, for chaining
* @throws IllegalArgumentException if {@code factories} is empty
*/
default WanderingTraderOffersBuilder addAll(Identifier id, Collection<? extends TradeOffers.Factory> factories) {
return pool(id, factories.size(), factories);
}
/**
* Adds trade offers to the offer list. All offers from {@code factories} are
* offered to each customer.
* @param id the ID to be assigned to this pool, to allow further modification
* @param factories the trade offer factories
* @return this builder, for chaining
* @throws IllegalArgumentException if {@code factories} is empty
*/
default WanderingTraderOffersBuilder addAll(Identifier id, TradeOffers.Factory... factories) {
return pool(id, factories.length, factories);
}
/**
* Adds trade offers to an existing pool identified by an ID.
*
* <p>See the constants for vanilla trade offer pool IDs that are always available.
* @param pool the pool ID
* @param factories the trade offer factories
* @return this builder, for chaining
* @throws IndexOutOfBoundsException if {@code pool} is out of bounds
*/
WanderingTraderOffersBuilder addOffersToPool(Identifier pool, TradeOffers.Factory... factories);
/**
* Adds trade offers to an existing pool identified by an ID.
*
* <p>See the constants for vanilla trade offer pool IDs that are always available.
* @param pool the pool ID
* @param factories the trade offer factories
* @return this builder, for chaining
* @throws IndexOutOfBoundsException if {@code pool} is out of bounds
*/
default WanderingTraderOffersBuilder addOffersToPool(Identifier pool, Collection<TradeOffers.Factory> factories) {
return addOffersToPool(pool, factories.toArray(TradeOffers.Factory[]::new));
}
}
}

View file

@ -17,30 +17,57 @@
package net.fabricmc.fabric.impl.object.builder;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.function.Consumer;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import it.unimi.dsi.fastutil.objects.Object2IntMap;
import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap;
import org.apache.commons.lang3.ArrayUtils;
import org.slf4j.LoggerFactory;
import org.apache.commons.lang3.tuple.Pair;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import net.minecraft.util.Identifier;
import net.minecraft.util.Util;
import net.minecraft.village.TradeOffers;
import net.minecraft.village.VillagerProfession;
import net.fabricmc.fabric.api.object.builder.v1.trade.TradeOfferHelper;
public final class TradeOfferInternals {
private static final Logger LOGGER = LoggerFactory.getLogger("fabric-object-builder-api-v1");
private TradeOfferInternals() {
}
/**
* Make the rebalanced profession map modifiable, then copy all vanilla
* professions' trades to prevent modifications from propagating to the rebalanced one.
*/
private static void initVillagerTrades() {
if (!(TradeOffers.REBALANCED_PROFESSION_TO_LEVELED_TRADE instanceof HashMap)) {
Map<VillagerProfession, Int2ObjectMap<TradeOffers.Factory[]>> map = new HashMap<>(TradeOffers.REBALANCED_PROFESSION_TO_LEVELED_TRADE);
for (Map.Entry<VillagerProfession, Int2ObjectMap<TradeOffers.Factory[]>> trade : TradeOffers.PROFESSION_TO_LEVELED_TRADE.entrySet()) {
if (!map.containsKey(trade.getKey())) map.put(trade.getKey(), trade.getValue());
}
TradeOffers.REBALANCED_PROFESSION_TO_LEVELED_TRADE = map;
}
}
// synchronized guards against concurrent modifications - Vanilla does not mutate the underlying arrays (as of 1.16),
// so reads will be fine without locking.
public static synchronized void registerVillagerOffers(VillagerProfession profession, int level, Consumer<List<TradeOffers.Factory>> factory) {
public static synchronized void registerVillagerOffers(VillagerProfession profession, int level, TradeOfferHelper.VillagerOffersAdder factory) {
Objects.requireNonNull(profession, "VillagerProfession may not be null.");
registerOffers(TradeOffers.PROFESSION_TO_LEVELED_TRADE.computeIfAbsent(profession, key -> new Int2ObjectOpenHashMap<>()), level, factory);
initVillagerTrades();
registerOffers(TradeOffers.PROFESSION_TO_LEVELED_TRADE.computeIfAbsent(profession, key -> new Int2ObjectOpenHashMap<>()), level, trades -> factory.onRegister(trades, false));
registerOffers(TradeOffers.REBALANCED_PROFESSION_TO_LEVELED_TRADE.computeIfAbsent(profession, key -> new Int2ObjectOpenHashMap<>()), level, trades -> factory.onRegister(trades, true));
}
public static synchronized void registerWanderingTraderOffers(int level, Consumer<List<TradeOffers.Factory>> factory) {
@ -63,4 +90,62 @@ public final class TradeOfferInternals {
Throwable loggingThrowable = new Throwable();
LOGGER.warn("TradeOfferHelper#refreshOffers does not do anything, yet it was called! Stack trace:", loggingThrowable);
}
public static class WanderingTraderOffersBuilderImpl implements TradeOfferHelper.WanderingTraderOffersBuilder {
private static final Object2IntMap<Identifier> ID_TO_INDEX = Util.make(new Object2IntOpenHashMap<>(), idToIndex -> {
idToIndex.put(BUY_ITEMS_POOL, 0);
idToIndex.put(SELL_SPECIAL_ITEMS_POOL, 1);
idToIndex.put(SELL_COMMON_ITEMS_POOL, 2);
});
private static final Map<Identifier, TradeOffers.Factory[]> DELAYED_MODIFICATIONS = new HashMap<>();
/**
* Make the trade list modifiable.
*/
static void initWanderingTraderTrades() {
if (!(TradeOffers.REBALANCED_WANDERING_TRADER_TRADES instanceof ArrayList)) {
TradeOffers.REBALANCED_WANDERING_TRADER_TRADES = new ArrayList<>(TradeOffers.REBALANCED_WANDERING_TRADER_TRADES);
}
}
@Override
public TradeOfferHelper.WanderingTraderOffersBuilder pool(Identifier id, int count, TradeOffers.Factory... factories) {
if (factories.length == 0) throw new IllegalArgumentException("cannot add empty pool");
if (count <= 0) throw new IllegalArgumentException("count must be positive");
Objects.requireNonNull(id, "id cannot be null");
if (ID_TO_INDEX.containsKey(id)) throw new IllegalArgumentException("pool id %s is already registered".formatted(id));
Pair<TradeOffers.Factory[], Integer> pool = Pair.of(factories, count);
initWanderingTraderTrades();
ID_TO_INDEX.put(id, TradeOffers.REBALANCED_WANDERING_TRADER_TRADES.size());
TradeOffers.REBALANCED_WANDERING_TRADER_TRADES.add(pool);
TradeOffers.Factory[] delayedModifications = DELAYED_MODIFICATIONS.remove(id);
if (delayedModifications != null) addOffersToPool(id, delayedModifications);
return this;
}
@Override
public TradeOfferHelper.WanderingTraderOffersBuilder addOffersToPool(Identifier pool, TradeOffers.Factory... factories) {
if (!ID_TO_INDEX.containsKey(pool)) {
DELAYED_MODIFICATIONS.compute(pool, (id, current) -> {
if (current == null) return factories;
return ArrayUtils.addAll(current, factories);
});
return this;
}
int poolIndex = ID_TO_INDEX.getInt(pool);
initWanderingTraderTrades();
Pair<TradeOffers.Factory[], Integer> poolPair = TradeOffers.REBALANCED_WANDERING_TRADER_TRADES.get(poolIndex);
TradeOffers.Factory[] modified = ArrayUtils.addAll(poolPair.getLeft(), factories);
TradeOffers.REBALANCED_WANDERING_TRADER_TRADES.set(poolIndex, Pair.of(modified, poolPair.getRight()));
return this;
}
}
}

View file

@ -5,6 +5,8 @@ accessible method net/minecraft/world/poi/PointOfInterestTypes register
extendable class net/minecraft/block/entity/BlockEntityType$BlockEntityFactory
accessible class net/minecraft/village/TradeOffers$TypeAwareBuyForOneEmeraldFactory
mutable field net/minecraft/village/TradeOffers REBALANCED_PROFESSION_TO_LEVELED_TRADE Ljava/util/Map;
mutable field net/minecraft/village/TradeOffers REBALANCED_WANDERING_TRADER_TRADES Ljava/util/List;
accessible method net/minecraft/entity/SpawnRestriction register (Lnet/minecraft/entity/EntityType;Lnet/minecraft/entity/SpawnRestriction$Location;Lnet/minecraft/world/Heightmap$Type;Lnet/minecraft/entity/SpawnRestriction$SpawnPredicate;)V

View file

@ -25,9 +25,12 @@ import com.mojang.brigadier.exceptions.SimpleCommandExceptionType;
import net.minecraft.entity.Entity;
import net.minecraft.entity.passive.WanderingTraderEntity;
import net.minecraft.item.Item;
import net.minecraft.item.ItemStack;
import net.minecraft.item.Items;
import net.minecraft.registry.Registries;
import net.minecraft.text.Text;
import net.minecraft.util.Identifier;
import net.minecraft.util.math.random.Random;
import net.minecraft.village.TradeOffer;
import net.minecraft.village.TradeOffers;
@ -38,16 +41,55 @@ import net.fabricmc.fabric.api.command.v2.CommandRegistrationCallback;
import net.fabricmc.fabric.api.object.builder.v1.trade.TradeOfferHelper;
public class VillagerTypeTest1 implements ModInitializer {
private static final Identifier FOOD_POOL_ID = ObjectBuilderTestConstants.id("food");
private static final Identifier THING_POOL_ID = ObjectBuilderTestConstants.id("thing");
@Override
public void onInitialize() {
TradeOfferHelper.registerVillagerOffers(VillagerProfession.ARMORER, 1, factories -> {
factories.add(new SimpleTradeFactory(new TradeOffer(new ItemStack(Items.GOLD_INGOT, 3), new ItemStack(Items.NETHERITE_SCRAP, 4), new ItemStack(Items.NETHERITE_INGOT), 2, 6, 0.15F)));
TradeOfferHelper.registerVillagerOffers(VillagerProfession.ARMORER, 1, (factories, rebalanced) -> {
Item scrap = rebalanced ? Items.NETHER_BRICK : Items.NETHERITE_SCRAP;
factories.add(new SimpleTradeFactory(new TradeOffer(new ItemStack(Items.GOLD_INGOT, 3), new ItemStack(scrap, 4), new ItemStack(Items.NETHERITE_INGOT), 2, 6, 0.15F)));
});
// Toolsmith is not rebalanced yet
TradeOfferHelper.registerVillagerOffers(VillagerProfession.TOOLSMITH, 1, (factories, rebalanced) -> {
Item scrap = rebalanced ? Items.NETHER_BRICK : Items.NETHERITE_SCRAP;
factories.add(new SimpleTradeFactory(new TradeOffer(new ItemStack(Items.GOLD_INGOT, 3), new ItemStack(scrap, 4), new ItemStack(Items.NETHERITE_INGOT), 2, 6, 0.15F)));
});
TradeOfferHelper.registerWanderingTraderOffers(1, factories -> {
factories.add(new SimpleTradeFactory(new TradeOffer(new ItemStack(Items.GOLD_INGOT, 3), new ItemStack(Items.NETHERITE_SCRAP, 4), new ItemStack(Items.NETHERITE_INGOT), 2, 6, 0.35F)));
});
TradeOfferHelper.registerRebalancedWanderingTraderOffers(builder -> {
builder.pool(
FOOD_POOL_ID,
5,
Registries.ITEM.stream().filter(item -> item.getFoodComponent() != null).map(
item -> new SimpleTradeFactory(new TradeOffer(new ItemStack(Items.NETHERITE_INGOT), new ItemStack(item), 3, 4, 0.15F))
).toList()
);
builder.addAll(
THING_POOL_ID,
new SimpleTradeFactory(new TradeOffer(new ItemStack(Items.NETHERITE_INGOT), new ItemStack(Items.MOJANG_BANNER_PATTERN), 1, 4, 0.15F))
);
builder.addOffersToPool(
TradeOfferHelper.WanderingTraderOffersBuilder.BUY_ITEMS_POOL,
new SimpleTradeFactory(new TradeOffer(new ItemStack(Items.BLAZE_POWDER, 1), new ItemStack(Items.EMERALD, 4), 3, 4, 0.15F)),
new SimpleTradeFactory(new TradeOffer(new ItemStack(Items.NETHER_WART, 5), new ItemStack(Items.EMERALD, 1), 3, 4, 0.15F)),
new SimpleTradeFactory(new TradeOffer(new ItemStack(Items.GOLDEN_CARROT, 4), new ItemStack(Items.EMERALD, 1), 3, 4, 0.15F))
);
builder.addOffersToPool(
TradeOfferHelper.WanderingTraderOffersBuilder.SELL_SPECIAL_ITEMS_POOL,
new SimpleTradeFactory(new TradeOffer(new ItemStack(Items.EMERALD, 6), new ItemStack(Items.BRUSH, 1), 1, 4, 0.15F)),
new SimpleTradeFactory(new TradeOffer(new ItemStack(Items.DIAMOND, 16), new ItemStack(Items.ELYTRA, 1), 1, 4, 0.15F)),
new SimpleTradeFactory(new TradeOffer(new ItemStack(Items.EMERALD, 3), new ItemStack(Items.LEAD, 2), 3, 4, 0.15F))
);
builder.addOffersToPool(
FOOD_POOL_ID,
new SimpleTradeFactory(new TradeOffer(new ItemStack(Items.NETHERITE_INGOT), new ItemStack(Items.EGG), 3, 4, 0.15F))
);
});
CommandRegistrationCallback.EVENT.register((dispatcher, registryAccess, environment) -> {
dispatcher.register(literal("fabric_applywandering_trades")
.then(argument("entity", entity()).executes(context -> {

View file

@ -30,7 +30,7 @@ import net.fabricmc.fabric.api.object.builder.v1.trade.TradeOfferHelper;
public class VillagerTypeTest2 implements ModInitializer {
@Override
public void onInitialize() {
TradeOfferHelper.registerVillagerOffers(VillagerProfession.ARMORER, 1, factories -> {
TradeOfferHelper.registerVillagerOffers(VillagerProfession.WEAPONSMITH, 1, factories -> {
factories.add(new SimpleTradeFactory(new TradeOffer(new ItemStack(Items.DIAMOND, 5), new ItemStack(Items.NETHERITE_INGOT), 3, 4, 0.15F)));
});
TradeOfferHelper.registerVillagerOffers(VillagerProfession.ARMORER, 1, factories -> {