Skip to content

Commit 0710b7b

Browse files
committed
Add transfer and stack fill operations
1 parent b7d2af7 commit 0710b7b

36 files changed

+1879
-684
lines changed

common/src/main/java/dev/terminalmc/clientsort/ClientSort.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,4 +22,6 @@ public class ClientSort {
2222
public static final String MOD_ID = "clientsort";
2323
public static final String MOD_NAME = "ClientSort";
2424
public static final ModLogger LOG = new ModLogger(MOD_NAME);
25+
26+
public static boolean debug;
2527
}

common/src/main/java/dev/terminalmc/clientsort/client/ClientSort.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,17 @@ public class ClientSort {
3333
public static final KeyMapping SORT_KEY = new KeyMapping(
3434
translationKey("key", "group.sort"), InputConstants.Type.MOUSE,
3535
InputConstants.MOUSE_BUTTON_MIDDLE, translationKey("key", "group"));
36+
public static final KeyMapping TRANSFER_KEY = new KeyMapping(
37+
translationKey("key", "group.transfer"), InputConstants.Type.KEYSYM,
38+
InputConstants.UNKNOWN.getValue(), translationKey("key", "group"));
39+
public static final KeyMapping FILL_STACKS_KEY = new KeyMapping(
40+
translationKey("key", "group.fillStacks"), InputConstants.Type.KEYSYM,
41+
InputConstants.UNKNOWN.getValue(), translationKey("key", "group"));
42+
public static final List<KeyMapping> KEYBINDS = List.of(
43+
SORT_KEY,
44+
TRANSFER_KEY,
45+
FILL_STACKS_KEY
46+
);
3647

3748
public static final List<ScheduledAction> SCHEDULED_ACTIONS = new ArrayList<>();
3849
public static class ScheduledAction {

common/src/main/java/dev/terminalmc/clientsort/client/config/Config.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,9 @@ public enum ExtraSlotScope {
106106
public static final boolean soundEnabledDefault = false;
107107
public boolean soundEnabled = soundEnabledDefault;
108108

109+
public static final boolean soundEnabledAllOpsDefault = false;
110+
public boolean soundEnabledAllOps = soundEnabledAllOpsDefault;
111+
109112
public static final String sortSoundDefault = "minecraft:block.note_block.xylophone";
110113
public String sortSound = sortSoundDefault;
111114
public transient @Nullable ResourceLocation sortSoundLoc = null;

common/src/main/java/dev/terminalmc/clientsort/client/gui/screen/ClothScreenProvider.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
package dev.terminalmc.clientsort.client.gui.screen;
1818

19+
import dev.terminalmc.clientsort.ClientSort;
1920
import dev.terminalmc.clientsort.client.order.CreativeSearchOrder;
2021
import dev.terminalmc.clientsort.client.order.SortOrder;
2122
import dev.terminalmc.clientsort.client.config.Config;
@@ -126,6 +127,13 @@ else if (val > Config.Options.interactionRateMax) return Optional.of(
126127
})
127128
.build());
128129

130+
general.addEntry(eb.startBooleanToggle(localized("option", "debugLogEnabled"),
131+
ClientSort.debug)
132+
.setTooltip(localized("option", "debugLogEnabled.tooltip"))
133+
.setDefaultValue(false)
134+
.setSaveConsumer(val -> ClientSort.debug = val)
135+
.build());
136+
129137
ConfigCategory sort = builder.getOrCreateCategory(localized("option", "sorting"));
130138

131139
sort.addEntry(eb.startSelector(localized("option", "sortOrder"),
@@ -164,6 +172,13 @@ else if (val > Config.Options.interactionRateMax) return Optional.of(
164172
.setSaveConsumer(val -> options.soundEnabled = val)
165173
.build());
166174

175+
sound.addEntry(eb.startBooleanToggle(localized("option", "soundEnabledAllOps"),
176+
options.soundEnabledAllOps)
177+
.setTooltip(localized("option", "soundEnabledAllOps.tooltip"))
178+
.setDefaultValue(Config.Options.soundEnabledAllOpsDefault)
179+
.setSaveConsumer(val -> options.soundEnabledAllOps = val)
180+
.build());
181+
167182
sound.addEntry(eb.startStrField(localized("option", "sortSound"),
168183
options.sortSound)
169184
.setDefaultValue(Config.Options.sortSoundDefault)
Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
/*
2+
* Copyright 2022 Siphalor
3+
* Copyright 2025 TerminalMC
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
package dev.terminalmc.clientsort.client.inventory.control;
19+
20+
import dev.terminalmc.clientsort.ClientSort;
21+
import dev.terminalmc.clientsort.client.compat.itemlocks.ItemLocksWrapper;
22+
import dev.terminalmc.clientsort.client.inventory.screen.ContainerScreenHelper;
23+
import dev.terminalmc.clientsort.client.inventory.control.client.ClientCreativeController;
24+
import dev.terminalmc.clientsort.client.inventory.control.client.ClientSurvivalController;
25+
import dev.terminalmc.clientsort.client.inventory.util.Scope;
26+
import dev.terminalmc.clientsort.client.inventory.control.server.ServerController;
27+
import dev.terminalmc.clientsort.client.order.SortOrder;
28+
import dev.terminalmc.clientsort.client.platform.Services;
29+
import net.minecraft.client.Minecraft;
30+
import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen;
31+
import net.minecraft.client.gui.screens.inventory.CreativeModeInventoryScreen;
32+
import net.minecraft.client.player.LocalPlayer;
33+
import net.minecraft.network.protocol.common.custom.CustomPacketPayload;
34+
import net.minecraft.world.inventory.Slot;
35+
import net.minecraft.world.item.*;
36+
37+
import java.util.*;
38+
39+
import static dev.terminalmc.clientsort.client.config.Config.options;
40+
41+
/**
42+
* Provides methods for manipulating the player's inventory or open container.
43+
* <p>
44+
* Constraints:
45+
* Note: A {@link SingleUseController} instance must be used one time,
46+
* immediately after creation, then promptly discarded, because the inventory
47+
* state is stored on initialization and not updated.
48+
* </ol>
49+
*/
50+
public abstract class SingleUseController {
51+
protected boolean hasOperated = false;
52+
protected final AbstractContainerScreen<?> screen;
53+
protected final ContainerScreenHelper<? extends AbstractContainerScreen<?>> screenHelper;
54+
/**
55+
* The slot that was hovered when sorting was triggered.
56+
*/
57+
protected final Slot originSlot;
58+
/**
59+
* A potentially noncontiguous sub-array of slots in the same scope as
60+
* {@link SingleUseController#originSlot}.
61+
* <p>
62+
* Must NOT be used by client-side operations to track and update simulation
63+
* state. Instead, use {@link SingleUseController#originScopeStacks}.
64+
*/
65+
protected final Slot[] originScopeSlots;
66+
/**
67+
* A 1:1 equivalent of {@link SingleUseController#originScopeSlots}, keeping
68+
* a logical record of the stack stored in each slot.
69+
*/
70+
protected final ItemStack[] originScopeStacks;
71+
/**
72+
* A potentially noncontiguous sub-array of slots not in the same scope as
73+
* {@link SingleUseController#originSlot}, but in still in either
74+
* {@link Scope#CONTAINER_INV} or {@link Scope#PLAYER_INV}.
75+
* <p>
76+
* Must NOT be used by client-side operations to track and update simulation
77+
* state. Instead, use {@link SingleUseController#originScopeStacks}.
78+
*/
79+
protected final Slot[] otherScopeSlots;
80+
/**
81+
* A 1:1 equivalent of {@link SingleUseController#otherScopeSlots}, keeping
82+
* a logical record of the stack stored in each slot.
83+
*/
84+
protected final ItemStack[] otherScopeStacks;
85+
86+
public SingleUseController(
87+
AbstractContainerScreen<?> screen,
88+
ContainerScreenHelper<? extends AbstractContainerScreen<?>> screenHelper,
89+
Slot originSlot
90+
) {
91+
this.screen = screen;
92+
this.screenHelper = screenHelper;
93+
this.originSlot = originSlot;
94+
95+
// Collect slots in origin scope
96+
Scope originScope = screenHelper.getScope(originSlot);
97+
originScopeSlots = findSlotsInScope(originScope);
98+
// Record stacks
99+
originScopeStacks = new ItemStack[originScopeSlots.length];
100+
for (int i = 0; i < originScopeSlots.length; i++) {
101+
originScopeStacks[i] = originScopeSlots[i].getItem();
102+
}
103+
104+
// Collect slots in other container scope, if any
105+
Scope otherScope = switch(originScope) {
106+
case PLAYER_INV -> Scope.CONTAINER_INV;
107+
case CONTAINER_INV -> Scope.PLAYER_INV;
108+
default -> Scope.INVALID;
109+
};
110+
otherScopeSlots = findSlotsInScope(otherScope);
111+
// Record stacks
112+
otherScopeStacks = new ItemStack[otherScopeSlots.length];
113+
for (int i = 0; i < otherScopeSlots.length; i++) {
114+
otherScopeStacks[i] = otherScopeSlots[i].getItem();
115+
}
116+
}
117+
118+
/**
119+
* Finds all the inventory menu slots that are in {@code scope}.
120+
*/
121+
private Slot[] findSlotsInScope(Scope scope) {
122+
LocalPlayer player = Minecraft.getInstance().player;
123+
if (scope == Scope.INVALID) return new Slot[0];
124+
125+
ArrayList<Slot> collectedSlots = new ArrayList<>();
126+
for (Slot slot : screen.getMenu().slots) {
127+
// Ignore slots in different scope
128+
if (screenHelper.getScope(slot) != scope) continue;
129+
// Ignore inaccessible slots
130+
if (player != null && !slot.mayPickup(player)) continue;
131+
// Ignore locked slots
132+
if (ItemLocksWrapper.isLocked(slot)) continue;
133+
// Slot is valid
134+
collectedSlots.add(slot);
135+
}
136+
137+
return collectedSlots.toArray(new Slot[0]);
138+
}
139+
140+
/**
141+
* @return {@code true} if this instance has not previously performed an
142+
* operation (and therefore is able to perform one).
143+
*/
144+
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
145+
protected boolean canOperate() {
146+
if (hasOperated) {
147+
ClientSort.LOG.warn("{} can only be used once!", this.getClass().getSimpleName());
148+
return false;
149+
} else {
150+
hasOperated = true;
151+
return true;
152+
}
153+
}
154+
155+
/**
156+
* @return an instance of {@link SingleUseController} optimized for the
157+
* current game state.
158+
*/
159+
public static SingleUseController getController(
160+
AbstractContainerScreen<?> screen,
161+
ContainerScreenHelper<? extends AbstractContainerScreen<?>> screenHelper,
162+
Slot originSlot,
163+
CustomPacketPayload.Type<?> payloadType
164+
) {
165+
if (options().serverAcceleratedSorting
166+
&& Services.PLATFORM.canSendToServer(payloadType)
167+
) {
168+
return new ServerController(screen, screenHelper, originSlot);
169+
}
170+
171+
//noinspection DataFlowIssue
172+
if (Minecraft.getInstance().player.isCreative()
173+
&& screen instanceof CreativeModeInventoryScreen
174+
) {
175+
return new ClientCreativeController(screen, screenHelper, originSlot);
176+
}
177+
178+
return new ClientSurvivalController(screen, screenHelper, originSlot);
179+
}
180+
181+
/**
182+
* Sorts the inventory according to {@code sortOrder}.
183+
*/
184+
public abstract void sort(SortOrder sortOrder);
185+
186+
/**
187+
* Transfers as many items as possible from the scope of the origin slot
188+
* to the other container or inventory, if it exists.
189+
*/
190+
public abstract void transfer();
191+
192+
/**
193+
* Uses items in the scope of the origin slot to complete as many partial
194+
* stacks as possible in the other container or inventory, if it exists.
195+
*/
196+
public abstract void fillStacks();
197+
}
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
/*
2+
* Copyright 2022 Siphalor
3+
* Copyright 2025 TerminalMC
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
package dev.terminalmc.clientsort.client.inventory.control.client;
19+
20+
import dev.terminalmc.clientsort.client.inventory.screen.ContainerScreenHelper;
21+
import dev.terminalmc.clientsort.client.inventory.control.SingleUseController;
22+
import dev.terminalmc.clientsort.client.order.SortContext;
23+
import dev.terminalmc.clientsort.client.order.SortOrder;
24+
import dev.terminalmc.clientsort.client.sound.SoundManager;
25+
import net.minecraft.client.Minecraft;
26+
import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen;
27+
import net.minecraft.world.inventory.Slot;
28+
29+
/**
30+
* Provides methods for manipulating the player's inventory or open container
31+
* via vanilla C2S inventory interaction packets.
32+
*/
33+
public abstract class ClientController extends SingleUseController {
34+
public ClientController(
35+
AbstractContainerScreen<?> screen,
36+
ContainerScreenHelper<? extends AbstractContainerScreen<?>> screenHelper,
37+
Slot originSlot
38+
) {
39+
super(screen, screenHelper, originSlot);
40+
}
41+
42+
/**
43+
* Uses vanilla C2S inventory interaction packets to sort the inventory.
44+
*/
45+
@Override
46+
public void sort(SortOrder sortOrder) {
47+
if (!canOperate()) return;
48+
49+
// Collect partial stacks
50+
collect();
51+
52+
// Create an array of ascending slot numbers
53+
int[] key = new int[originScopeSlots.length];
54+
for (int i = 0; i < key.length; i++) {
55+
key[i] = i;
56+
}
57+
58+
// Sort the array of slot numbers to create a sorting key which
59+
// defines, for each slot of originScopeSlots, the index in
60+
// originScopeStacks from which the new stack should be retrieved
61+
key = sortOrder.sort(
62+
key,
63+
originScopeStacks,
64+
new SortContext(Minecraft.getInstance().level)
65+
);
66+
67+
// Prepare sounds
68+
boolean playSound = SoundManager.shouldPlayWhenSorting();
69+
if (playSound) SoundManager.resetForCount(
70+
SoundManager.estimateSortSounds(originScopeStacks));
71+
72+
// Sort
73+
sort(key, playSound);
74+
}
75+
76+
/**
77+
* Uses vanilla C2S inventory interaction packets to collect partial stacks
78+
* into the smallest possible number of slots.
79+
*/
80+
protected abstract void collect();
81+
82+
/**
83+
* Uses vanilla C2S inventory interaction packets to sort the inventory
84+
* according to {@code key}.
85+
*/
86+
protected abstract void sort(int[] key, boolean playSound);
87+
}

0 commit comments

Comments
 (0)