Compare commits

...
Author SHA1 Message Date
Caedis cced6fa21b Merge branch 'master' into caedis/autocraft-throttle 2026-08-01 12:48:24 -05:00
Caedis 30eec77a20 spotless 2026-07-27 14:04:52 -05:00
Caedis 75e480f0c7 Merge remote-tracking branch 'origin/master' into caedis/autocraft-throttle 2026-07-27 13:48:41 -05:00
Caedis 6ae40a1b36 feat(autocraft): global momentum with switch penalty
Replace per-recipe speed memory with a single global momentum. A recipe
change now reduces speed by a penalty instead of resetting, so returning
keeps most momentum. Retune for feel: faster start (300ms), calmer top
(100ms floor, bulk 4), gentler decay (0.88).
2026-07-18 03:47:29 -05:00
Caedis 540caeaae3 feat(autocraft): drop instant first craft
A recipe now starts at the full start delay instead of a free instant
craft, so crafting begins slow. Collapses the first-craft special case
into the default ramp value.
2026-07-18 03:18:36 -05:00
Caedis 502a688a75 feat(autocraft): per-recipe speed memory and bulk at max
Track each recipe's ramp in a per-run map so switching away and back
resumes its accumulated speed instead of restarting slow. At floor
speed, run crafts in bulk (8 per tick) so large orders finish quickly.
Single-craft accounting is kept so the inventory-full stop still holds.
2026-07-18 03:10:19 -05:00
Caedis cbaef9cee0 fix(autocraft): stop when crafted output cannot be taken
craft() reported success from the result slot having a stack, even
when a full inventory blocked the shift-click pickup. That made the
auto-craft loop treat it as progress and never terminate. Check the
result slot after the click: if the output is still there it wasn't
taken, so break.
2026-07-18 03:02:31 -05:00
Caedis a40289ca65 feat(autocraft): throttle crafting with speed ramp
Crafts now run one item at a time instead of in batches of 64, with
an interruptible sleep between each craft driven by CraftRampThrottle.
The delay starts high and decays toward a floor as long as the same
recipe keeps crafting, and resets to instant whenever the recipe
changes, giving auto-craft a visible slow-start-then-speed-up feel
instead of instant bulk crafting.
2026-07-17 13:59:31 -05:00
Caedis 9ce2aa40bb feat(autocraft): add CraftRampThrottle ramp math
Pure per-craft delay ramp for throttling auto-craft: first craft
instant, then geometric decay from 500ms toward a 50ms floor,
resetting when the RecipeId changes. Wired into AutoCrafting in a
later change.
2026-07-17 13:58:14 -05:00
4 changed files with 169 additions and 3 deletions

No files matched your search

@@ -29,6 +29,7 @@ public class AutoCraftingManager {
final List<BookmarkItem> initialItems = prepareInitialItems(math, getInventoryItems(guiContainer));
boolean processed = false;
boolean changed = false;
final CraftRampThrottle throttle = new CraftRampThrottle();
StackInfo.pauseItemDamageSound(true);
@@ -48,9 +49,19 @@ public class AutoCraftingManager {
if (handler != null && handler.canCraft(guiContainer)) {
long multiplier = entry.getValue();
while (multiplier > 0 && !interrupted(guiContainer)
&& handler.craft(guiContainer, (int) Math.min(64, multiplier))) {
multiplier -= 64;
while (multiplier > 0 && !interrupted(guiContainer)) {
final CraftRampThrottle.Tick tick = throttle.next(entry.getKey());
sleepInterruptibly(tick.delayMs, guiContainer);
if (interrupted(guiContainer)) break;
boolean crafted = false;
for (int i = 0; i < tick.crafts && multiplier > 0 && !interrupted(guiContainer); i++) {
if (!handler.craft(guiContainer, 1)) break;
multiplier -= 1;
crafted = true;
}
if (!crafted) break; // output couldn't be taken (e.g. inventory full)
}
craft = multiplier != entry.getValue();
@@ -86,6 +97,19 @@ public class AutoCraftingManager {
return interrupted() || guiContainer != NEIClientUtils.getGuiContainer();
}
private void sleepInterruptibly(long delayMs, GuiContainer guiContainer) {
long remaining = delayMs;
while (remaining > 0 && !interrupted(guiContainer)) {
final long chunk = Math.min(20L, remaining);
try {
Thread.sleep(chunk);
} catch (InterruptedException ignored) {
return;
}
remaining -= chunk;
}
}
private List<BookmarkItem> prepareInitialItems(RecipeChainMath math, ItemStackAmount inventory) {
final List<BookmarkItem> initialItems = new ArrayList<>();
@@ -0,0 +1,49 @@
package codechicken.nei.recipe;
import java.util.Objects;
import codechicken.nei.recipe.Recipe.RecipeId;
/**
* Per-craft delay ramp for auto-crafting. Crafting a recipe repeatedly speeds up (delay decays geometrically toward a
* floor). Momentum is global: switching to a different recipe reduces speed by a penalty but does not reset it, so
* going back to a recipe resumes with most of its momentum. At floor (max) speed crafts are batched. Pure logic; does
* not sleep.
*/
public class CraftRampThrottle {
public static final long START_DELAY_MS = 300L; // delay before the 1st craft
public static final long FLOOR_DELAY_MS = 100L; // fastest allowed (cap)
public static final double DECAY = 0.88D; // per-craft speedup
public static final double SWITCH_PENALTY = 2.0D; // momentum lost on recipe change
public static final int BULK_CRAFTS = 4; // crafts per tick once maxed out
/** Delay to wait before a craft, and how many crafts that tick covers. */
public static final class Tick {
public final long delayMs;
public final int crafts;
Tick(long delayMs, int crafts) {
this.delayMs = delayMs;
this.crafts = crafts;
}
}
private RecipeId current;
private long delayMs = START_DELAY_MS;
/** Next tick for {@code id}: decays on repeat, slows (but keeps momentum) on a recipe change. */
public Tick next(RecipeId id) {
if (!Objects.equals(id, this.current)) {
if (this.current != null) {
this.delayMs = Math.min(START_DELAY_MS, Math.round(this.delayMs * SWITCH_PENALTY));
}
this.current = id;
}
final long delay = this.delayMs;
this.delayMs = Math.max(FLOOR_DELAY_MS, Math.round(delay * DECAY));
return new Tick(delay, delay <= FLOOR_DELAY_MS ? BULK_CRAFTS : 1);
}
}
@@ -123,6 +123,12 @@ public class DefaultOverlayHandler implements IOverlayHandler {
if (craftingSlot.getHasStack() && craftingSlot.canTakeStack(firstGui.mc.thePlayer)) {
FastTransferManager.clickSlot(firstGui, craftingSlot.slotNumber, 0, 1);
// Output still present means it wasn't taken (e.g. inventory full); stop.
if (craftingSlot.getHasStack()) {
break;
}
craft = true;
}
@@ -0,0 +1,87 @@
package codechicken.nei.recipe;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.mock;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import codechicken.nei.recipe.CraftRampThrottle.Tick;
import codechicken.nei.recipe.Recipe.RecipeId;
class CraftRampThrottleTest {
private static Tick rampToFloor(CraftRampThrottle throttle, RecipeId id) {
Tick tick = null;
for (int i = 0; i < 100; i++) {
tick = throttle.next(id);
}
return tick;
}
@Test
@DisplayName("first craft of a recipe uses the start delay and is single")
void firstCraftUsesStartDelay() {
CraftRampThrottle throttle = new CraftRampThrottle();
RecipeId id = mock(RecipeId.class);
Tick tick = throttle.next(id);
assertEquals(CraftRampThrottle.START_DELAY_MS, tick.delayMs);
assertEquals(1, tick.crafts);
}
@Test
@DisplayName("same recipe decays monotonically and clamps at floor")
void rampDecaysToFloor() {
CraftRampThrottle throttle = new CraftRampThrottle();
RecipeId id = mock(RecipeId.class);
long prev = Long.MAX_VALUE;
for (int i = 0; i < 5; i++) {
long delay = throttle.next(id).delayMs;
assertTrue(delay <= prev, "delay should not increase while ramping");
assertTrue(delay >= CraftRampThrottle.FLOOR_DELAY_MS, "delay should not drop below floor");
prev = delay;
}
assertEquals(CraftRampThrottle.FLOOR_DELAY_MS, rampToFloor(throttle, id).delayMs);
}
@Test
@DisplayName("at floor speed, crafts happen in bulk")
void bulkAtFloorSpeed() {
CraftRampThrottle throttle = new CraftRampThrottle();
RecipeId id = mock(RecipeId.class);
Tick tick = rampToFloor(throttle, id);
assertEquals(CraftRampThrottle.FLOOR_DELAY_MS, tick.delayMs);
assertEquals(CraftRampThrottle.BULK_CRAFTS, tick.crafts);
}
@Test
@DisplayName("crafts stay single while still ramping")
void singleWhileRamping() {
CraftRampThrottle throttle = new CraftRampThrottle();
RecipeId id = mock(RecipeId.class);
assertEquals(1, throttle.next(id).crafts);
assertEquals(1, throttle.next(id).crafts);
}
@Test
@DisplayName("recipe change reduces momentum but does not fully reset")
void recipeChangeReducesMomentum() {
CraftRampThrottle throttle = new CraftRampThrottle();
RecipeId a = mock(RecipeId.class);
RecipeId b = mock(RecipeId.class);
// Ramp A to full speed (floor).
assertEquals(CraftRampThrottle.FLOOR_DELAY_MS, rampToFloor(throttle, a).delayMs);
// Switching to B slows down, but keeps momentum: slower than floor, faster than a cold start.
long afterSwitch = throttle.next(b).delayMs;
assertTrue(afterSwitch > CraftRampThrottle.FLOOR_DELAY_MS, "switch should reduce momentum");
assertTrue(afterSwitch < CraftRampThrottle.START_DELAY_MS, "switch should not fully reset");
}
}