Skip to content

Commit

Permalink
API (#8)
Browse files Browse the repository at this point in the history
- Made some methods and types available through an API.
- Added an entrypoint to define villager trades from outside mods
  • Loading branch information
Estecka authored Jan 18, 2024
1 parent 747d4c4 commit 77612c7
Show file tree
Hide file tree
Showing 13 changed files with 227 additions and 62 deletions.
7 changes: 5 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,15 @@ The map's item name, or its translation key, is used to tell apart different typ


## Technical details
- This mod runs under the assumption that villagers have at most 2 trades per level. It will always try to enforce this layout when rerolling.

- If a villager is unable to generate all registered trades for a level, it will be replaced with an empty trade. With vanilla trades, this should only ever happen to cartographers, who are unable to generate explorer maps in worlds with no structures.
These paddings are required to ensure trades are rerolled with one of equivalent level; a trade's position in the list is the only indication to its level.
Placeholder trades will never take the place of a valid trade; they will only show up if all other options are exhausted.

- The "Demand Bonus" game mechanic is mostly removed, because the demand bonus data is deleted along with the offers that are rerolled. Any effect it may still have is uncertain.

- Depleted rerolls have a chance to yield duplicate trades.

## For developpers
By default, shifting-Wares assumes 2 trades per level, and pulls its trade pools from the same place as Vanilla.

If you have a mod that changes any of that, Shifting-Wares has an API you can use to specify the trade pools and layout that should be used instead.
4 changes: 4 additions & 0 deletions changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,7 @@ Initial release

## 1.1
- Added a gamerules that allows cartographers to regenerate maps that have been sold at least once.

# v2
- Made some methods and types available through an API.
- Added an entrypoint to define villager trades from outside mods
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,6 @@ loader_version=0.15.3
fabric_version=0.91.0+1.20.1

# Mod Properties
mod_version=1.1.0
mod_version=2.0.0
maven_group=tk.estecka.shiftingwares
archives_base_name=shifting-wares
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package tk.estecka.shiftingwares;

public interface IVillagerEntityDuck
import tk.estecka.shiftingwares.api.IHasItemCache;

public interface IVillagerEntityDuck
extends IHasItemCache
{
MapTradesCache shiftingwares$GetTradeCache();
MapTradesCache shiftingwares$GetItemCache();
}
34 changes: 28 additions & 6 deletions src/main/java/tk/estecka/shiftingwares/MapTradesCache.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import java.util.Optional;
import java.util.Set;
import org.jetbrains.annotations.Nullable;
import net.minecraft.entity.Entity;
import net.minecraft.item.FilledMapItem;
import net.minecraft.item.ItemStack;
import net.minecraft.item.Items;
Expand All @@ -18,8 +19,10 @@
import net.minecraft.text.TranslatableTextContent;
import net.minecraft.village.TradeOffer;
import net.minecraft.village.TradeOfferList;
import tk.estecka.shiftingwares.api.PersistentItemCache;

public class MapTradesCache
public class MapTradesCache
implements PersistentItemCache
{
static public final int DATA_FORMAT = 1;
static public final String FORMAT_KEY = "shifting-wares:data_format";
Expand Down Expand Up @@ -54,14 +57,33 @@ static public String FindCacheKey(ItemStack item){
return key;
}

public Optional<ItemStack> GetCachedMap(String key){
if (this.cachedItems.containsKey(key))
/**
* @return Empty if the entity is allowed to forget the item, or does not
* remember it. Otherwise, returns the corresponding cached item.
*/
static public Optional<ItemStack> Resell(Entity entity, String cacheKey){
if (entity instanceof IVillagerEntityDuck villager)
{
MapTradesCache cache = villager.shiftingwares$GetItemCache();
Optional<ItemStack> cachedMap = cache.GetCachedItem(cacheKey);
if (cachedMap.isEmpty() || (cache.HasSold(cacheKey) && entity.getWorld().getGameRules().getBoolean(ShiftingWares.MAP_RULE)))
return Optional.empty();
else
return cachedMap;
}

return Optional.empty();
}

public Optional<ItemStack> GetCachedItem(String key){
ItemStack item = this.cachedItems.get(key);
if (item != null)
return Optional.of(this.cachedItems.get(key));
else
return Optional.empty();
}

public void AddCachedMap(String key, ItemStack mapItem){
public void AddCachedItem(String key, ItemStack mapItem){
Integer neoId=FilledMapItem.getMapId(mapItem);
if (!cachedItems.containsKey(key))
ShiftingWares.LOGGER.info("New map trade: #{} @ {}", neoId, key);
Expand Down Expand Up @@ -105,10 +127,10 @@ public void FillCacheFromTrades(TradeOfferList offers){
ShiftingWares.LOGGER.info("Marked map as sold: #{} @ {}", FilledMapItem.getMapId(sellItem), cacheKey);
}

var oldItem = this.GetCachedMap(cacheKey);
var oldItem = this.GetCachedItem(cacheKey);
if (oldItem.isEmpty() || !ItemStack.areEqual(sellItem, oldItem.get())){
ShiftingWares.LOGGER.warn("Caught a map trade that wasn't properly cached: #{} @ {}", FilledMapItem.getMapId(sellItem), cacheKey);
this.AddCachedMap(cacheKey, sellItem);
this.AddCachedItem(cacheKey, sellItem);
}
}
}
Expand Down
25 changes: 23 additions & 2 deletions src/main/java/tk/estecka/shiftingwares/ShiftingWares.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,21 @@
import net.fabricmc.api.ModInitializer;
import net.fabricmc.fabric.api.gamerule.v1.GameRuleFactory;
import net.fabricmc.fabric.api.gamerule.v1.GameRuleRegistry;
import net.fabricmc.loader.api.FabricLoader;
import net.minecraft.entity.passive.VillagerEntity;
import net.minecraft.item.ItemStack;
import net.minecraft.village.TradeOffer;
import net.minecraft.village.TradeOffers;
import net.minecraft.world.GameRules;
import net.minecraft.world.GameRules.BooleanRule;

import tk.estecka.shiftingwares.api.ITradeLayoutProvider;
import tk.estecka.shiftingwares.TradeLayouts.VanillaTradeLayout;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class ShiftingWares implements ModInitializer
public class ShiftingWares
implements ModInitializer
{
static public final Logger LOGGER = LoggerFactory.getLogger("Shifting-Wares");

Expand All @@ -21,7 +27,22 @@ public class ShiftingWares implements ModInitializer

static public final TradeOffer PLACEHOLDER_TRADE = new TradeOffer(ItemStack.EMPTY, ItemStack.EMPTY, ItemStack.EMPTY, 0, 0, 0, 0, 0);

static public final ITradeLayoutProvider VANILLA_LAYOUT = new VanillaTradeLayout();

static public List<TradeOffers.Factory[]> GetTradeLayout(VillagerEntity villager){
var providers = FabricLoader.getInstance().getEntrypoints("shifting-wares", ITradeLayoutProvider.class);

for (var p : providers) {
var layout = p.GetTradeLayout(villager);
if (layout != null)
return layout;
}

return VANILLA_LAYOUT.GetTradeLayout(villager);
}

@Override
public void onInitialize() {
// Static initialization
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package tk.estecka.shiftingwares.TradeLayouts;

import java.util.ArrayList;
import java.util.List;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import net.minecraft.entity.passive.VillagerEntity;
import net.minecraft.village.TradeOffers;
import net.minecraft.village.VillagerData;
import net.minecraft.village.VillagerProfession;
import net.minecraft.village.TradeOffers.Factory;
import tk.estecka.shiftingwares.ShiftingWares;
import tk.estecka.shiftingwares.api.ITradeLayoutProvider;

public class VanillaTradeLayout
implements ITradeLayoutProvider
{
public List<Factory[]> GetTradeLayout(VillagerEntity villager){
List<Factory[]> layout = new ArrayList<>();
VillagerProfession job = villager.getVillagerData().getProfession();
int jobLevel = villager.getVillagerData().getLevel();

Int2ObjectMap<Factory[]> jobPool = TradeOffers.PROFESSION_TO_LEVELED_TRADE.get(job);

if (jobPool == null){
ShiftingWares.LOGGER.error("No trade pool for job {}.", job);
return null;
}

for (int lvl=VillagerData.MIN_LEVEL; lvl<=jobLevel; ++lvl)
{
var pool = jobPool.get(lvl);
if (pool == null)
ShiftingWares.LOGGER.error("Missing pool for job {} lvl.{}", job, lvl);
else for (int i=0; i<2 && i<pool.length; ++i)
layout.add(pool);
}

return layout;
}

}
83 changes: 45 additions & 38 deletions src/main/java/tk/estecka/shiftingwares/TradeShuffler.java
Original file line number Diff line number Diff line change
@@ -1,15 +1,12 @@
package tk.estecka.shiftingwares;

import java.util.ArrayList;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import it.unimi.dsi.fastutil.ints.IntArrayList;
import it.unimi.dsi.fastutil.ints.IntList;
import java.util.IdentityHashMap;
import java.util.List;
import net.minecraft.entity.passive.VillagerEntity;
import net.minecraft.util.math.random.Random;
import net.minecraft.village.TradeOffer;
import net.minecraft.village.TradeOfferList;
import net.minecraft.village.TradeOffers;
import net.minecraft.village.VillagerData;
import net.minecraft.village.TradeOffers.Factory;
import net.minecraft.village.VillagerProfession;

Expand All @@ -19,11 +16,10 @@ public class TradeShuffler
private final boolean depletedOnly;

private final VillagerProfession job;
private final int jobLevel;

private final Random random;
private final TradeOfferList offers;
private final Int2ObjectMap<Factory[]> jobPool;
private final List<Factory[]> tradeLayout;
private final MapTradesCache tradeCache;

public TradeShuffler(VillagerEntity villager, boolean depletedOnly)
Expand All @@ -33,39 +29,30 @@ public TradeShuffler(VillagerEntity villager, boolean depletedOnly)

this.offers = villager.getOffers();
this.job = villager.getVillagerData().getProfession();
this.jobLevel = villager.getVillagerData().getLevel();
this.random = villager.getRandom();
this.tradeCache = ((IVillagerEntityDuck)villager).shiftingwares$GetTradeCache();
this.tradeCache = ((IVillagerEntityDuck)villager).shiftingwares$GetItemCache();

this.jobPool = TradeOffers.PROFESSION_TO_LEVELED_TRADE.get(job);
this.tradeLayout = ShiftingWares.GetTradeLayout(villager);
}

public void Reroll(){
if (jobPool == null){
ShiftingWares.LOGGER.error("No trade pool for job {}. Villager will not be rerolled.", job);
if (tradeLayout == null){
ShiftingWares.LOGGER.error("Missing layout, villager will not be rerolled: {} ({})", job, villager);
return;
}

tradeCache.FillCacheFromTrades(offers);

IntList slotLevels = new IntArrayList();
for (int lvl=VillagerData.MIN_LEVEL; lvl<=this.jobLevel; ++lvl)
if (jobPool.containsKey(lvl)){
var pool = jobPool.get(lvl);
if (pool == null)
ShiftingWares.LOGGER.error("Missing pool for job {} lvl.{}", job, jobLevel);
else for (int i=0; i<2 && i<pool.length; ++i)
slotLevels.add(lvl);
}

for (int i=offers.size()-1; slotLevels.size()<=i; --i)
// Trim superfluous trades
for (int i=offers.size()-1; tradeLayout.size()<=i; --i)
if (shouldReroll(i))
offers.remove(i);
while(offers.size() < slotLevels.size())

// Reserve space for new trades
while(offers.size() < tradeLayout.size())
offers.add(ShiftingWares.PLACEHOLDER_TRADE);

for (int tradeLvl=VillagerData.MIN_LEVEL; tradeLvl<=jobLevel; ++tradeLvl)
DuplicataAwareReroll(tradeLvl, slotLevels);
DuplicataAwareReroll();

tradeCache.FillCacheFromTrades(offers);
}
Expand All @@ -77,21 +64,41 @@ public boolean shouldReroll(int tradeIndex){
;
}

private void DuplicataAwareReroll(int tradeLvl, IntList slotLevels){
Factory[] levelPool = jobPool.get(tradeLvl);
boolean missingSome = false;
static private List<Factory>[] MutableCopy(List<Factory[]> layout){
IdentityHashMap<Factory[], ArrayList<Factory>> mutablePools = new IdentityHashMap<>();
mutablePools.put(null, new ArrayList<>(0));

for (var pool : layout)
if (!mutablePools.containsKey(pool))
{
var mpool = new ArrayList<Factory>(pool.length);
for (var f : pool)
mpool.add(f);
mutablePools.put(pool, mpool);
}

var randomPool = new ArrayList<Factory>(levelPool.length);
for (var f : levelPool)
randomPool.add(f);
@SuppressWarnings("unchecked")
List<Factory>[] workspace = new List[layout.size()];
for (int i=0; i<workspace.length; ++i)
workspace[i] = mutablePools.get(layout.get(i));

return workspace;
}

private void DuplicataAwareReroll(){
List<Factory>[] mutableLayout = MutableCopy(this.tradeLayout);
boolean missingSome = false;

for (int i=0; i<offers.size(); ++i)
if (tradeLvl==slotLevels.getInt(i) && shouldReroll(i)) {
if (shouldReroll(i))
{
var pool = mutableLayout[i];
TradeOffer offer = null;
while (offer == null && !randomPool.isEmpty()) {
int roll = random.nextInt(randomPool.size());
offer = randomPool.get(roll).create(villager, random);
randomPool.remove(roll);

while (offer == null && !pool.isEmpty()) {
int roll = random.nextInt(pool.size());
offer = pool.get(roll).create(villager, random);
pool.remove(roll);
}
if (offer == null){
offer = ShiftingWares.PLACEHOLDER_TRADE;
Expand All @@ -102,7 +109,7 @@ private void DuplicataAwareReroll(int tradeLvl, IntList slotLevels){
}

if (missingSome)
ShiftingWares.LOGGER.warn("Failed to generate some trade offers for {} lvl.{} ({})", job, tradeLvl, villager);
ShiftingWares.LOGGER.warn("Failed to generate some trade offers for job {} ({})", job, villager);
}

}
9 changes: 9 additions & 0 deletions src/main/java/tk/estecka/shiftingwares/api/IHasItemCache.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package tk.estecka.shiftingwares.api;

/**
* Implemented by VillagerEntity
*/
public interface IHasItemCache
{
PersistentItemCache shiftingwares$GetItemCache();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package tk.estecka.shiftingwares.api;

import java.util.List;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import net.minecraft.entity.passive.VillagerEntity;
import net.minecraft.village.TradeOffers.Factory;

/**
* Should be implemented by mods that want to use shifting-ware's entrypoint.
*/
public interface ITradeLayoutProvider
{
/**
* @return For each hypothetical slot in the villager's offer listing, this
* provides the pool of trades that can go into that slot. If this instance
* does not know the layout of a specific villager, it can return null in
* order to fall back to the vanilla layout.
*
* The same pool can be assigned to multiple slots in order to avoid
* duplicates. Pool equality is evaluated by identity, i.e by comparing
* pointers.
*
* The size of the returned list needs not match the size of the villager's
* current lising; it should return the intended layout. The villager's
* listing may have its size adjusted so as to match the return value.
*
* This should take into account the villager's current level, and only
* provide trade slots which the villager has unlocked.
*
*/
@Nullable List<@NotNull Factory @NotNull[]> GetTradeLayout(VillagerEntity villager);
}
Loading

0 comments on commit 77612c7

Please sign in to comment.