-
Notifications
You must be signed in to change notification settings - Fork 197
Dev Q&A
You don't need to worry about any synchronization and serialization code for fields.
Check funtions of each annotation here. LDLib Wiki
You can create them via NotifiableItemStackHandler
, NotifiableFluidTank
, NotifiableEnergyContainer
. In general, you should always use them.
They notify all listeners of internal changes to improve performance.
Parameters include io
and capabilityIO
:
-
io
: Whether it is regarded as input or output during recipe processing? -
caabilityIO
: Whether player can use hopper, pipe interacte storage?
NOTE: In general, they are created as final
fields (That's what we need for a syncdata system), set their base arguments in the constructor (you can pass args for subclasses to modify).
If you do not need to add a storage that can be used for recipe processing and providing capability, you can just use ItemStackTransfer
, FluidStorage
, they are more lightweight.
The client update is always present and you can override the clientTick()
, which works as well as 1.12.
But for the sake of performance, our machines are no longer always in an Tickable state. We introduce ITickSubscription
for managed tick logic.
Understand the basic concept of subscribing to periodic updates when they are needed and unsubscribe them when they are not.
For example, Automatic output of the machine requires periodic output of internal items to adjacent inventory. But most of the time this logic doesn't need to be executed. If there is no item inside the machine, or the automatic output is not set to active, or there is no adjacent block that can accept the item. Lets look at how we implement it in QuantumChest
.
@Getter @Persisted @DescSynced
protected boolean autoOutputItems;
@Persisted @DropSaved
protected final NotifiableItemStackHandler cache; // inner inventory
protected TickableSubscription autoOutputSubs;
protected ISubscription exportItemSubs;
// update subscription, subscribe if tick logic subscription is required, unsubscribe otherwise.
protected void updateAutoOutputSubscription() {
var outputFacing = getOutputFacingItems(); // get output facing
if ((isAutoOutputItems() && !cache.isEmpty()) // inner item non empty
&& outputFacing != null // has output facing
&& ItemTransferHelper.getItemTransfer(getLevel(), getPos().relative(outputFacing), outputFacing.getOpposite()) != null) { // adjacent block has inventory.
autoOutputSubs = subscribeServerTick(autoOutputSubs, this::checkAutoOutput); // subscribe tick logic
} else if (autoOutputSubs != null) { // unsubscribe tick logic
autoOutputSubs.unsubscribe();
autoOutputSubs = null;
}
}
// output to nearby block.
protected void checkAutoOutput() {
if (getOffsetTimer() % 5 == 0) {
if (isAutoOutputItems() && getOutputFacingItems() != null) {
cache.exportToNearby(getOutputFacingItems());
}
updateAutoOutputSubscription(); // dont foget to check if it's still available
}
}
@Override
public void onLoad() {
super.onLoad();
if (getLevel() instanceof ServerLevel serverLevel) {
// you cant call ItemTransferHelper.getItemTransfer while chunk is loading, so lets defer it next tick.
serverLevel.getServer().tell(new TickTask(0, this::updateAutoOutputSubscription));
}
// add a listener to listen the changes of inner inventory. (for ex, if inventory not empty anymore, we may need to unpdate logic)
exportItemSubs = cache.addChangedListener(this::updateAutoOutputSubscription);
}
@Override
public void onUnload() {
super.onUnload(); //autoOutputSubs will be released automatically when machine unload
if (exportItemSubs != null) { //we should mannually release it.
exportItemSubs.unsubscribe();
exportItemSubs = null;
}
}
// For any change may affect the logic to invoke updateAutoOutputSubscription at a time
@Override
public void setAutoOutputItems(boolean allow) {
this.autoOutputItems = allow;
updateAutoOutputSubscription();
}
@Override
public void setOutputFacingItems(Direction outputFacing) {
this.outputFacingItems = outputFacing;
updateAutoOutputSubscription();
}
@Override
public void onNeighborChanged(Block block, BlockPos fromPos, boolean isMoving) {
super.onNeighborChanged(block, fromPos, isMoving);
updateAutoOutputSubscription();
}
I know the code is a kinda long, but it's for performance, and thanks to the SyncData system, we've eliminated a lot of synchronization code, so please sacrifice a little for better performance.
To improve its generality, RecipeLogic has been rewritten from 1.19 to support inputs and outputs other than eu, item, and fluid.
The new RecipeLogic no longer handles overclocked and parallel logic, but instead delegates it to the machine via IRecipeLogicMachine
:
/**
* Override it to modify recipe on the fly e.g. applying overclock, change chance, etc
* @param recipe recipe from detected from GTRecipeType
* @return modified recipe.
* null -- this recipe is unavailable
*/
@Nullable
GTRecipe modifyRecipe(GTRecipe recipe);
In general, a simple electric overclocking can be done this way. For details, see OverclockingLogic
and RecipeHelper
public @Nullable GTRecipe modifyRecipe(GTRecipe recipe) {
if (RecipeHelper.getRecipeEUtTier(recipe) > getTier()) {
return null;
}
return RecipeHelper.applyOverclock(getDefinition().getOverclockingLogic(), recipe, getMaxVoltage());
}
Parallel is also not complicated to implement. Let's take the generator
as an example
public @Nullable GTRecipe modifyRecipe(GTRecipe recipe) {
var EUt = RecipeHelper.getOutputEUt(recipe); // get the eut of the recipe
if (EUt > 0) {
// calculate the max parallel limitation.
var maxParallel = (int)(Math.min(energyContainer.getOutputVoltage(), GTValues.V[overclockTier]) / EUt);
while (maxParallel > 0) {
var copied = recipe.copy(ContentModifier.multiplier(maxParallel)); // copy and apply parallel, it will affect all recipes' contents and duration time.
if (copied.matchRecipe(this)) { // If the machine has enough ingredients, return copied recipe.
copied.duration = copied.duration / maxParallel; // we dont need to modify duration.
return copied;
}
maxParallel /= 2; //Trying to halve the number of parallelism,
}
}
return null;
}
Make sure you use the correct units for fluid.
FluidHelper.getBucket(); // return 1000 for forge. return 81000 for fabric.
Fabric hasn't got capability sys, but you can access like this.
FluidTransferHelper.getFluidTransfer(...);
ItemTransferHelper.getItemTransfer(...);
GTCapabilityHelper.getRecipeLogic(...)
GTCapabilityHelper.getControllable(...)
GTCapabilityHelper.getCoverable(...)
GTCapabilityHelper.getToolable(...)
GTCapabilityHelper.getWorkable(...)
GTCapabilityHelper.getElectricItem(...)
GTCapabilityHelper.getEnergyContainer(...)
When the annotated fields updated (synced from server) will schedule chunk rendering update.
@DescSynced @RequireRerender
protected boolean autoOutputItems;
When the annotated fields updated (synced from server) will call the listener method.
@DescSynced @UpdateListener(methodName = "onTransformUpdated")
private boolean isTransformUp;
@SuppressWarnings("unused")
private void onTransformUpdated(boolean newValue, boolean oldValue) {
scheduleRenderUpdate();
updateEnergyContainer(newValue);
}