Skip to content

Commit 59c8d93

Browse files
authored
Merge pull request #85 from CTNH-Team/fix/apotheosis-nbt
fix: 修复神话装备无法从create发报机取出的bug; 增加create发报机robustness避免类似bug
2 parents 04c70fd + 4b0c1d7 commit 59c8d93

3 files changed

Lines changed: 281 additions & 0 deletions

File tree

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package io.github.cpearl0.ctnhcore.mixin.apotheosis;
2+
3+
import net.minecraft.world.item.ItemStack;
4+
5+
import dev.shadowsoffire.apotheosis.adventure.socket.SocketHelper;
6+
import dev.shadowsoffire.apotheosis.adventure.socket.SocketedGems;
7+
import dev.shadowsoffire.apotheosis.adventure.socket.gem.GemInstance;
8+
import org.spongepowered.asm.mixin.Mixin;
9+
import org.spongepowered.asm.mixin.injection.At;
10+
import org.spongepowered.asm.mixin.injection.Inject;
11+
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
12+
13+
/**
14+
* Forces UUID generation for Apotheosis gems on the server side.
15+
* <p>
16+
* Apotheosis normally generates gem UUIDs lazily on the client side during tooltip
17+
* rendering. This can cause Create's logistics system to fail because the client's
18+
* UUIDs don't match the server's.
19+
* <p>
20+
* This mixin forces UUID generation immediately before gems are written to NBT in
21+
* {@code SocketHelper.setGems()}, ensuring the server-side NBT includes UUIDs.
22+
*/
23+
@Mixin(value = SocketHelper.class, remap = false)
24+
public class SocketHelperMixin {
25+
26+
/**
27+
* Before SocketHelper.setGems() serializes gems to NBT, we iterate through each
28+
* GemInstance and call getUUIDs() to ensure UUIDs are generated and written into
29+
* the gemStack's NBT. When setGems() later calls gemStack.save(), the UUIDs will
30+
* be included in the serialized data.
31+
*/
32+
@Inject(method = "setGems", at = @At("HEAD"), remap = false)
33+
private static void ctnh$preGenerateUUIDs(ItemStack stack, SocketedGems gems, CallbackInfo ci) {
34+
for (GemInstance inst : gems) {
35+
if (inst.isValid()) {
36+
// Calling getUUIDs() triggers GemItem.getOrCreateUUIDs()
37+
// which generates UUIDs if missing and writes them directly
38+
// into gemStack's CompoundTag. Since setGems() will call
39+
// gemStack.save() later, the UUIDs will be persisted.
40+
inst.getUUIDs();
41+
}
42+
}
43+
}
44+
}
Lines changed: 235 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,235 @@
1+
package io.github.cpearl0.ctnhcore.mixin.create;
2+
3+
import net.minecraft.world.item.ItemStack;
4+
5+
import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
6+
import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
7+
import com.simibubi.create.content.logistics.BigItemStack;
8+
import com.simibubi.create.content.logistics.packager.InventorySummary;
9+
import com.simibubi.create.content.logistics.stockTicker.StockKeeperRequestScreen;
10+
import org.spongepowered.asm.mixin.Mixin;
11+
import org.spongepowered.asm.mixin.Shadow;
12+
import org.spongepowered.asm.mixin.injection.At;
13+
import org.spongepowered.asm.mixin.injection.Inject;
14+
import org.spongepowered.asm.mixin.injection.Redirect;
15+
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
16+
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
17+
18+
import java.util.ArrayList;
19+
import java.util.List;
20+
21+
/**
22+
* Fixes Create's Stock Keeper request screen so it correctly handles
23+
* modded items whose {@code ItemStack} NBT is mutated on the client
24+
* during tooltip rendering (Apotheosis socketed gems, GTCEU tool
25+
* stats, etc.). Those mutations cause the displayed stack to drift
26+
* away from the server snapshot, breaking
27+
* {@link StockKeeperRequestScreen#getOrderForItem} which uses strict
28+
* NBT equality.
29+
*
30+
* <p>
31+
* <b>Strategy:</b> use the flat index of a click position in
32+
* {@code displayedItems} as the lookup key. On every
33+
* {@code refreshSearchResults} RETURN we build a parallel
34+
* {@code ctnh$originList} of fresh origin stacks (one per entry,
35+
* indexed identically to {@code displayedItems}). When the user
36+
* clicks an item, the {@code copyWithCount} {@code @Redirect} in
37+
* {@code mouseClicked} records the flat index of the clicked entry,
38+
* and the TAIL injection stores it in {@code ctnh$orderFlatIndices}.
39+
* On REBUILD we re-link each order's {@code stack} to the new origin
40+
* at its stored index. {@code getOrderForItem} and
41+
* {@code forcedEntries.getCountOf} are wrapped to look up by index
42+
* instead of NBT, so client-side NBT mutations no longer affect
43+
* matching.
44+
*
45+
* <p>
46+
* <b>Why instance fields, not static:</b> static state outlives the
47+
* screen instance: if the player closes and reopens the stock
48+
* ticker, the lists carry stale entries. Instance fields tie the
49+
* parallel state to the screen lifetime, eliminating cross-session
50+
* pollution.
51+
*/
52+
@Mixin(value = StockKeeperRequestScreen.class, remap = false)
53+
public class StockKeeperRequestScreenMixin {
54+
55+
@Shadow
56+
private List<List<BigItemStack>> displayedItems;
57+
58+
@Shadow
59+
private List<BigItemStack> itemsToOrder;
60+
61+
private final List<ItemStack> ctnh$originList = new ArrayList<>();
62+
63+
private final List<Integer> ctnh$orderFlatIndices = new ArrayList<>();
64+
65+
private int ctnh$pendingFlatIdx = -2;
66+
67+
private int ctnh$orderSizeAtClickStart = -1;
68+
69+
private static ItemStack ctnh$copyStack(ItemStack src) {
70+
if (src == null || src.isEmpty()) {
71+
return src;
72+
}
73+
return src.copy();
74+
}
75+
76+
private int ctnh$findFlatIndex(ItemStack target) {
77+
if (this.displayedItems == null || target == null) {
78+
return -1;
79+
}
80+
int flat = 0;
81+
for (List<BigItemStack> row : this.displayedItems) {
82+
if (row == null) {
83+
continue;
84+
}
85+
for (BigItemStack entry : row) {
86+
if (entry != null && entry.stack == target) {
87+
return flat;
88+
}
89+
flat++;
90+
}
91+
}
92+
return -1;
93+
}
94+
95+
@Inject(method = "refreshSearchResults", at = @At("RETURN"), remap = false)
96+
private void ctnh$rebuildOriginList(boolean scrollBackUp, CallbackInfo ci) {
97+
ctnh$originList.clear();
98+
if (this.displayedItems != null) {
99+
for (List<BigItemStack> row : this.displayedItems) {
100+
if (row == null) {
101+
continue;
102+
}
103+
for (BigItemStack entry : row) {
104+
if (entry == null || entry.stack == null || entry.stack.isEmpty()) {
105+
ctnh$originList.add(null);
106+
} else {
107+
ctnh$originList.add(ctnh$copyStack(entry.stack));
108+
}
109+
}
110+
}
111+
}
112+
113+
// Keep ctnh$orderFlatIndices aligned with itemsToOrder.
114+
// Pad with -1 if we somehow missed an add (e.g. scroll/schematic
115+
// paths bypass our copyWithCount redirect). Trim from the end if
116+
// itemsToOrder shrank (e.g. revalidateOrders called removeAll).
117+
if (this.itemsToOrder != null) {
118+
int target = this.itemsToOrder.size();
119+
while (ctnh$orderFlatIndices.size() < target) {
120+
ctnh$orderFlatIndices.add(-1);
121+
}
122+
while (ctnh$orderFlatIndices.size() > target) {
123+
ctnh$orderFlatIndices.remove(ctnh$orderFlatIndices.size() - 1);
124+
}
125+
// Re-link orders to origins at the stored indices.
126+
for (int i = this.itemsToOrder.size() - 1; i >= 0; i--) {
127+
int idx = i < ctnh$orderFlatIndices.size() ? ctnh$orderFlatIndices.get(i) : -1;
128+
if (idx < 0 || idx >= ctnh$originList.size() || ctnh$originList.get(idx) == null) {
129+
// No reliable origin to re-link to. Drop the order.
130+
this.itemsToOrder.remove(i);
131+
if (i < ctnh$orderFlatIndices.size()) {
132+
ctnh$orderFlatIndices.remove(i);
133+
}
134+
} else {
135+
this.itemsToOrder.get(i).stack = ctnh$copyStack(ctnh$originList.get(idx));
136+
}
137+
}
138+
} else {
139+
ctnh$orderFlatIndices.clear();
140+
}
141+
}
142+
143+
/**
144+
* Redirects the {@code ItemStack.copyWithCount(1)} call that
145+
* builds the {@code BigItemStack} appended to
146+
* {@code itemsToOrder} in {@code mouseClicked}. We build the new
147+
* stack from the pristine origin at the same flat index, and
148+
* record that index so the TAIL {@code @Inject} can store it in
149+
* {@link #ctnh$orderFlatIndices}.
150+
*/
151+
@Redirect(
152+
method = "mouseClicked",
153+
remap = false,
154+
at = @At(
155+
value = "INVOKE",
156+
target = "Lnet/minecraft/world/item/ItemStack;copyWithCount(I)Lnet/minecraft/world/item/ItemStack;",
157+
ordinal = 0))
158+
private ItemStack ctnh$redirectCopyWithCountForOrder(ItemStack displayed, int count) {
159+
int flatIdx = ctnh$findFlatIndex(displayed);
160+
ctnh$pendingFlatIdx = flatIdx;
161+
if (flatIdx >= 0 && flatIdx < ctnh$originList.size() && ctnh$originList.get(flatIdx) != null) {
162+
return ctnh$copyStack(ctnh$originList.get(flatIdx)).copyWithCount(count);
163+
}
164+
return displayed.copyWithCount(count);
165+
}
166+
167+
@Inject(method = "mouseClicked", at = @At("HEAD"), remap = false)
168+
private void ctnh$onMouseClickedHead(double mouseX, double mouseY, int button,
169+
CallbackInfoReturnable<Boolean> cir) {
170+
ctnh$orderSizeAtClickStart = (this.itemsToOrder == null) ? -1 : this.itemsToOrder.size();
171+
ctnh$pendingFlatIdx = -2;
172+
}
173+
174+
@Inject(method = "mouseClicked", at = @At("TAIL"), remap = false)
175+
private void ctnh$onMouseClickedTail(double mouseX, double mouseY, int button,
176+
CallbackInfoReturnable<Boolean> cir) {
177+
if (ctnh$pendingFlatIdx < 0 || ctnh$orderSizeAtClickStart < 0) {
178+
ctnh$pendingFlatIdx = -2;
179+
ctnh$orderSizeAtClickStart = -1;
180+
return;
181+
}
182+
if (this.itemsToOrder != null) {
183+
int now = this.itemsToOrder.size();
184+
if (now == ctnh$orderSizeAtClickStart + 1) {
185+
while (ctnh$orderFlatIndices.size() < now) {
186+
ctnh$orderFlatIndices.add(-1);
187+
}
188+
ctnh$orderFlatIndices.set(now - 1, ctnh$pendingFlatIdx);
189+
}
190+
}
191+
ctnh$pendingFlatIdx = -2;
192+
ctnh$orderSizeAtClickStart = -1;
193+
}
194+
195+
@WrapOperation(
196+
method = "renderItemEntry",
197+
remap = false,
198+
at = @At(
199+
value = "INVOKE",
200+
target = "Lcom/simibubi/create/content/logistics/stockTicker/StockKeeperRequestScreen;getOrderForItem(Lnet/minecraft/world/item/ItemStack;)Lcom/simibubi/create/content/logistics/BigItemStack;"))
201+
private BigItemStack ctnh$wrapGetOrderForItem(StockKeeperRequestScreen self, ItemStack displayed,
202+
Operation<BigItemStack> original) {
203+
int flatIdx = ctnh$findFlatIndex(displayed);
204+
if (flatIdx < 0) {
205+
return original.call(self, displayed);
206+
}
207+
if (this.itemsToOrder != null) {
208+
for (int i = 0; i < this.itemsToOrder.size() && i < ctnh$orderFlatIndices.size(); i++) {
209+
if (ctnh$orderFlatIndices.get(i) == flatIdx) {
210+
return this.itemsToOrder.get(i);
211+
}
212+
}
213+
}
214+
return null;
215+
}
216+
217+
@WrapOperation(
218+
method = "renderItemEntry",
219+
remap = false,
220+
at = @At(
221+
value = "INVOKE",
222+
target = "Lcom/simibubi/create/content/logistics/packager/InventorySummary;getCountOf(Lnet/minecraft/world/item/ItemStack;)I"))
223+
private int ctnh$wrapGetCountOf(InventorySummary forcedEntries, ItemStack displayed,
224+
Operation<Integer> original) {
225+
int flatIdx = ctnh$findFlatIndex(displayed);
226+
if (flatIdx < 0 || flatIdx >= ctnh$originList.size()) {
227+
return original.call(forcedEntries, displayed);
228+
}
229+
ItemStack origin = ctnh$originList.get(flatIdx);
230+
if (origin == null) {
231+
return original.call(forcedEntries, displayed);
232+
}
233+
return original.call(forcedEntries, origin);
234+
}
235+
}

src/main/resources/ctnhcore.mixins.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
"aecs.EmiPluginMixin",
1010
"alexcaves.MagnetUtilMixin",
1111
"apotheosis.EarthsBoonEnchantMixin",
12+
"apotheosis.SocketHelperMixin",
1213
"create.SpoutCategoryMixin",
1314
"createmetallurgy.CreateMetallurgyJEIMixin",
1415
"createmetallurgy.CrucibleBlockEntityMixin",
@@ -47,6 +48,7 @@
4748
"vintageimprovements.LatheRotatingBlockMixin"
4849
],
4950
"client": [
51+
"create.StockKeeperRequestScreenMixin",
5052
"gtceu.fix.ContentMixin",
5153
"sophisticatedcore.SyncContainerStacksMessageMixin",
5254
"sophisticatedcore.SyncSlotStackMessageMixin"

0 commit comments

Comments
 (0)