22 KiB
MCC inventory sync issues report
Sources read
- GitHub issue #3112: https://github.com/MCCTeam/Minecraft-Console-Client/issues/3112
- GitHub issue #3126: https://github.com/MCCTeam/Minecraft-Console-Client/issues/3126
- GitHub issue #3124: https://github.com/MCCTeam/Minecraft-Console-Client/issues/3124
- Related issue referenced by #3124: https://github.com/MCCTeam/Minecraft-Console-Client/issues/3119
- Commit inspected:
3c59bfe13abc18291506bec59118b5bd7509f4eb(Experimental inventory sync)
Short version
There are three separate but related problems in the reports:
- Before commit
3c59bfe,/inventory container listcould be correct while/inventory player liststayed stale. The script usedGetPlayerInventory(), so it believed moved or trashed items were still in the player inventory and repeated the same actions. - Commit
3c59bfetried to fix that by mirroring the player-inventory part of an open container back into inventory id0. That addressed the stale player list in the first test, but it introduced or exposedx0ghost items because the mirrored player inventory can share the sameItemobject reference as the open container, and later local shift-click prediction can mutate that object toCount = 0. - Issue #3124 is mainly a Script Scheduler/reconnect duplication issue. It matters here because the posted
WarpAFK.csscripts show the user process and the workaround they wrote for the stale inventory problem, but the scheduler bug itself is not fixed by3c59bfe.
What the issue is exactly
MCC keeps inventories in a dictionary keyed by server window id. Inventory id 0 is the player inventory. Open containers use separate ids, for example the /trash or /çöp GUI and the /is chest N GUI.
In Minecraft protocol, an open container window contains both:
- the container's own top slots
- a mirrored copy of the player's normal 36 inventory slots
For example:
- A 27-slot chest (
Generic_9x3) has 63 total slots in MCC: 27 chest slots plus 36 mirrored player slots. - A 36-slot trash GUI (
Generic_9x4) has 72 total slots in MCC: 36 trash slots plus 36 mirrored player slots. - The mirrored player slots begin after the container's own top slots.
The user's old script scans GetPlayerInventory() to decide what to do, then it clicks the mirrored player slot in the open container:
- For
/çöp, the GUI has 36 top slots, socontainerSlot = playerSlot + 27. Example: player slot36maps to container slot63. - For
/is chest N, the GUI has 27 top slots, socontainerSlot = playerSlot + 18. Example: player slot36maps to container slot54.
Before 3c59bfe, DoWindowAction() updated only the clicked container object. If the script clicked the open trash/chest container, MCC's current container snapshot was updated, but inventory id 0 was not updated from that container's mirrored player slots. Therefore:
/inventory container listshowed the item removed./inventory player liststill showed the old item.GetPlayerInventory()returned the stale id0state.- The bot returned to trash/storage mode and clicked the same logical item slots again.
This is the core bug from #3112.
How to reproduce #3112 manually by hand
These steps avoid the automation script and use only MCC commands. They are based on the user's server and process from #3112.
Prerequisites:
- Connect MCC to
play.ronemacraft.com. - Use Minecraft
1.20.4. - Use a build before the experimental inventory sync fix, for example the reported MCC version 445 or another build before
3c59bfe. - Have inventory handling enabled.
- Have at least one item that the server can move into the trash GUI or an island chest.
Trash GUI reproduction:
- Stand still in the Opskyblock/AFK area where the user observed the problem.
- Put an ore item in the normal player inventory, for example
EmeraldOre,IronOre,GoldOre, orDiamondOre. - Run
/inventory player list. - Note the player slot that contains the ore. For a hotbar item this is usually in slots
36through44. - Send
/çöpor the server's/trashalias. - Wait until MCC logs that a virtual trash inventory opened.
- Run
/inventory container list. - Find the same item in the container's mirrored player section. For the 36-slot trash GUI, use
containerSlot = playerSlot + 27. - Run
/inventory container click <containerSlot> ShiftClick. - Run
/inventory container listagain. The container view should show that the item was removed or moved. - Run
/inventory player list. - On the broken builds, the player list can still show the item in the old player slot.
- Close the container with
/inventory container close. - Open
/çöpagain and repeat the same click. The server-side item is already gone, but MCC still thinks it exists because id0is stale.
Island chest reproduction:
- Put any non-ore item in the player inventory.
- Run
/inventory player listand note the player slot. - Send
/opskyblockif needed by the server flow. - Send
/is chest 21. - Wait until the chest opens.
- For the 27-slot chest GUI, compute
containerSlot = playerSlot + 18. - Run
/inventory container click <containerSlot> ShiftClick. - Run
/inventory container list. The open container view should reflect the move. - Run
/inventory player list. On affected builds, the item can still be listed in the player inventory. - The user's script then sees
hasOther = true, tries the next chest, and repeats until its own workaround stops it.
Why moving or warping seems to fix it:
- The user said
/spawnto/warp afk5refreshes/inventory player list. - That is consistent with the server resending player inventory or slot state during a world/lobby/teleport transition.
- It is not a useful workaround for the user because leaving the AFK location resets the 30-minute reward counter.
How issue #3124 fits in
Issue #3124 is not the same inventory defect. It is a continuation of #3119, where Script Scheduler can re-run login-triggered scripts during /reco or reconnect spam while old script instances keep running.
The relevant Script Scheduler config from #3124 is:
Trigger_On_Login = true- One task sends
/login password. - Another task runs
script WarpAFK.cs.
Manual scheduler reproduction:
- Enable
ChatBot.ScriptScheduler. - Add a login-triggered task that starts a script, like the
WarpAFK.csshown in #3124. - Connect to the server.
- Repeatedly run
/reco, or force several disconnect/reconnect cycles. - Watch the log for multiple script instances continuing at once.
- Expected behavior would be that old script instances are stopped before a new login-triggered instance starts.
- Actual behavior from #3119/#3124 was repeated commands such as
/opskyblock,/pay,/is chest 21, and/warp afk5, sometimes before MCC considers itself fully connected.
Why this matters for the inventory report:
- The first
WarpAFK.csin #3124 contains slot-recorder logic (doneSlotTypes,confirmedPSlots,failedPSlots) specifically because/inventory player listwas unreliable. - The later
WarpAFK.csposted in #3124 is simpler because the user believed the new player inventory sync worked. It goes back to scanningGetPlayerInventory()forPlayerItems(). - That means the experimental inventory sync directly changed how the script could be written, even though the reconnect duplication is a separate bug.
What changed in commit 3c59bfe
Commit 3c59bfe13abc18291506bec59118b5bd7509f4eb is titled Experimental inventory sync.
Files changed:
MinecraftClient/McClient.csMinecraftClient/Resources/Translations/Translations.resxMinecraftClient/Resources/Translations/Translations.Designer.cs
Main inventory changes:
-
Fixed a merge-order bug in
TryMergeSlot().- Before the commit, the full-merge branch set
item.Count = 0before adding it to the destination stack. That meant the destination received0. - After the commit, it adds
item.CounttocurItem.Countfirst, then setsitem.Count = 0. - Current code:
MinecraftClient/McClient.cs, around lines 1998-2014.
- Before the commit, the full-merge branch set
-
Added mirrored player-inventory range detection.
TryGetMirroredPlayerInventoryRange()assumes non-player containers end with 36 mirrored player inventory slots.TryGetMirroredPlayerInventorySlot()maps a container window slot back to a player inventory slot usingplayerInventorySlot = windowSlot - firstWindowSlot + 9.- Current code:
MinecraftClient/McClient.cs, around lines 2065-2095.
-
Added
SetPlayerInventorySlot().- This updates inventory id
0for a mapped player slot. - It removes the slot when the incoming item is null or empty.
- It skips work when
AreSameInventorySlot()says the old and new slot are equivalent. - Current code:
MinecraftClient/McClient.cs, around lines 2097-2125.
- This updates inventory id
-
Added sync from open containers to player inventory id
0.SyncPlayerInventorySlotFromWindow()syncs one mapped mirrored slot.SyncPlayerInventorySlotsFromWindow()syncs all mirrored slots.DoWindowAction()now callsSyncPlayerInventorySlotsFromWindow(inventory)before sending the click packet.OnWindowItems()now syncs the whole mirrored range and dispatchesOnInventoryUpdate(0)if changed.OnSetSlot()now syncs a mapped mirrored slot and dispatchesOnInventoryUpdate(0)if changed.- Current code:
MinecraftClient/McClient.cs, around lines 2128-2152, 2896, and 3931-3989.
-
Added empty-item filtering to
OnWindowItems().- The commit removes items from incoming full window contents when
Item.IsEmptyis true. - Current code:
MinecraftClient/McClient.cs, around lines 3935-3937.
- The commit removes items from incoming full window contents when
-
Added cursor cleanup for left/right click predictions.
- If cursor slot
-1reaches zero count, it is removed. - Current code:
MinecraftClient/McClient.cs, around lines 2219-2221 and 2280-2282.
- If cursor slot
-
Added shift-click cleanup.
- If a shift-clicked source item reaches zero count and is still present in the clicked container dictionary, it is removed.
- Current code:
MinecraftClient/McClient.cs, around lines 2868-2872.
Translation changes:
cmd.inventory.shiftclickchanged fromShift clicking slot {0} in window #{1}toShift.cmd.inventory.shiftrightclickchanged fromShift right-clicking slot {0} in window #{1}toShift right.- This fixes the nested format-string problem from #3126. The outer message is
{0} clicking slot {1} in window #{2}, so the action label must not contain its own{0}and{1}placeholders. - Current command formatting is in
MinecraftClient/Commands/Inventory.cs, around lines 355-365.
Why the commit helped #3112
The main stale-list failure was that id 0 did not follow the mirrored player section inside nonzero container windows.
The new sync code makes this happen:
- Open
/çöpor/is chest N. - The open container's full slot list includes the player's mirrored 36 slots.
OnWindowItems()copies that mirrored section into player inventory id0.- After a click,
DoWindowAction()locally predicts the container change and syncs mirrored slots into id0. - Bot scripts reading
GetPlayerInventory()now see the result of the container click.
This is why the test build mentioned in #3112 appeared to work for the user's old script.
Why the commit did not fully work
The commit can leave x0 items in /inventory player list.
The most likely concrete cause is object aliasing plus the equality check in SetPlayerInventorySlot().
Detailed flow:
OnWindowItems()receives an open container, for example/is chest 21.SyncPlayerInventorySlotsFromWindow()maps the mirrored player slots from that container into inventory id0.SetPlayerInventorySlot()stores the sameItemobject reference from the container dictionary into the player inventory dictionary. It does not clone the item.- The user shift-clicks a mirrored player slot in the open container.
- If the destination already has the same stackable item,
TryMergeSlot()fully merges the source stack. - In that full-merge branch, the code sets the source
item.Count = 0. - Because player inventory id
0may hold the sameItemobject reference, the player inventory entry now also hasCount = 0. DoWindowAction()removes the source slot from the open container and callsSyncPlayerInventorySlotsFromWindow().- The sync sees the window slot is now empty and tries to set the mapped player slot to
null. SetPlayerInventorySlot()callsAreSameInventorySlot(previousItem, null).AreSameInventorySlot()returns true when the previous item is empty and the new item is null.- Because it returns true,
SetPlayerInventorySlot()exits early and does not remove the existing dictionary entry. /inventory player listiterates the dictionary directly and prints the item, so it appears asx0.
This matches #3126 very closely:
- The user bought one grassy soil from
/marketor/shop. - The script stored it in
/is chest. - The first item may have moved into an empty chest slot, so no
x0appeared. - The user bought the same item again.
- The second item could merge into the existing chest stack, causing
TryMergeSlot()to set the source count to zero. - The player inventory dictionary retained the zero-count item, so
/inventory player listshowedx0.
It also explains why the issue is intermittent:
- Moving into an empty destination slot uses
StoreInNewSlot(), which removes the source slot but does not set the source item count to zero. - Fully merging into an existing stack uses
TryMergeSlot(), which does set the source item count to zero. - Therefore the visible result depends on whether the item moved into an empty slot or merged into an existing compatible stack.
How to reproduce #3126 manually by hand
Use the experimental build containing 3c59bfe.
Server-specific reproduction from the issue:
- Connect to
play.ronemacraft.comon Minecraft1.20.4. - Go to the Opskyblock area where
/marketor/shopis available. - Buy exactly one item from
/marketor/shop. The user specifically mentioned grassy soil. - Run
/inventory player listand note the player slot containing the item. - Open an island chest with
/is chest 21. - Use
/inventory container listto confirm the item appears in the mirrored player section. - Compute the container slot as
playerSlot + 18for a 27-slot chest. - Run
/inventory container click <containerSlot> ShiftClick. - Close the container with
/inventory container close. - Run
/inventory player list. The item may disappear correctly. - Buy the same item again, exactly one item.
- Open the same
/is chest 21again. - Shift-click the second item into the chest using the same slot mapping.
- Run
/inventory player list. - If the second item merged into the existing stack in the chest, the player list can show that item as
x0.
Generic reproduction that should not require this specific server:
- Use a build containing
3c59bfe. - Connect to any server where inventory actions work.
- Put a partial stack of a stackable item in a chest, for example 1 dirt in a chest slot.
- Put 1 dirt in the player's inventory.
- Open the chest.
- Run
/inventory player listand note the player slot of the dirt. - For a normal 27-slot chest, compute
containerSlot = playerSlot + 18. - Run
/inventory container click <containerSlot> ShiftClick. - Run
/inventory player listquickly after the click. - The expected correct result is no dirt in the old player slot.
- The suspected broken result is a retained entry with
x0 Dirt.
If this generic reproduction does not show x0, add logging or inspect the dictionaries immediately after DoWindowAction(). Server correction packets can sometimes mask the display timing, but the code path itself can retain a zero-count item.
Is this just an issue on this server?
The stale /inventory player list issue is not purely a RonemaCraft server bug. It is a real MCC state-model gap:
- MCC had separate snapshots for player inventory id
0and open container ids. - Open containers include mirrored player inventory slots.
- Before the commit, MCC did not sync those mirrored slots back into id
0. - Any server or plugin GUI that updates only the open container window can expose this mismatch.
RonemaCraft makes the issue easy to notice because:
- It uses plugin GUIs for
/çöp,/is chest,/market, and daily rewards. - The user's bot depends on
GetPlayerInventory()while staying stationary in an AFK area. - Warping away refreshes inventory but resets the AFK reward counter, so the stale state persists long enough to hurt the workflow.
The x0 issue from 3c59bfe is also not inherently server-only. It follows from MCC's own local prediction and reference sharing. The server's workflow makes it reproducible because buying the same item twice and storing it into the same chest naturally creates a merge-into-existing-stack case.
The server can still affect how often the bug appears:
- Some servers send extra full inventory refreshes that mask stale id
0state. - Some plugin GUIs send unusual item stacks, custom names, or zero-count placeholders.
- Some server flows delay or omit player-window updates while only updating the open plugin window.
So the right classification is:
- #3112: general MCC design gap, exposed strongly by this server's plugin GUI workflow.
- #3126: general bug in the experimental fix, easiest to trigger with this server's repeated market/chest process.
- #3124/#3119: Script Scheduler/reconnect lifecycle bug, separate from inventory sync.
How the inventory system worked before this commit
Before 3c59bfe, the relevant flow was:
-
MCC initializes the inventory dictionary with player inventory id
0.ClearInventories()createsinventories[0] = new Container(0, ContainerType.PlayerInventory, "Player Inventory").- Current code is around
MinecraftClient/McClient.cslines 2960-2970.
-
When the server opens a container, MCC adds another
Containerunder that server window id.- The container type comes from the protocol menu type id.
- The title comes from the packet title.
- For 1.20.4, this happens through
OpenWindowhandling inProtocol18.cs.
-
Each
Containerhas a flatItemsdictionary.- Key: slot id.
- Value:
Item. - Empty slots are normally absent from the dictionary.
-
Container slot counts include the player's mirrored inventory section.
Generic_9x3is 63 total slots: 27 top slots plus 36 player slots.Generic_9x4is 72 total slots: 36 top slots plus 36 player slots.Generic_9x6is 90 total slots: 54 top slots plus 36 player slots.PlayerInventoryis 46 total slots.- Current values are in
MinecraftClient/Inventory/ContainerTypeExtensions.cs, around lines 10-40.
-
/inventory player listreads only inventory id0.- It iterates
inventory.Itemsdirectly. - It prints every dictionary entry, including an item whose count is zero.
- Current command code is around
MinecraftClient/Commands/Inventory.cslines 320-327.
- It iterates
-
/inventory container listreads the highest open inventory id when no id is supplied.- This is usually the foreground open server GUI.
- It does not read inventory id
0.
-
OnWindowItems()replaced only the addressed inventory's item dictionary.- Before this commit, it did not filter zero-count items.
- Before this commit, it did not sync mirrored player slots into inventory id
0. - Before this commit, it dispatched only
OnInventoryUpdate(inventoryID).
-
OnSetSlot()updated only the addressed inventory id, except special protocol cases.inventoryID == 254maps to player inventory id0.inventoryID == 255and slot-1maps to the cursor item stored in player inventory slot-1.- Newer 1.21.2+
SetPlayerInventorypackets map directly to id0, but the reported server uses 1.20.4, so that packet path is not available.
-
DoWindowAction()locally predicted the result of clicks before sending the click packet.- For a nonzero window id, it modified that open container's
Items. - It did not also update player inventory id
0. - It sent
ClickWindowwith the calculated changed slots and state id.
- For a nonzero window id, it modified that open container's
-
Bot APIs reflect this model.
GetPlayerInventory()returns inventory id0.GetInventories()returns the full dictionary of player plus open containers.OnInventoryUpdate(int inventoryId)tells bots which inventory id changed.- Before the commit, a container mirror change did not imply an
OnInventoryUpdate(0)event.
The practical consequence before the commit:
- If a script needed live item state while a container was open,
/inventory container listorGetInventories()[openContainerId]was more reliable thanGetPlayerInventory(). - Scripts had to manually translate mirrored container slots back to player slots, exactly as the user's workaround did.
GetPlayerInventory()was safe only after the server actually refreshed inventory id0.
Suggested direction for a real fix
This report is not a patch, but the code evidence points to a safer direction:
-
Do not store the same mutable
Iteminstance in both the open container dictionary and player inventory id0.- Clone items when syncing from a container mirror into player inventory.
-
Make
SetPlayerInventorySlot()remove an existing dictionary entry when the new item is null or empty, even if the old entry is already empty.- The dictionary state matters. An empty item object in the dictionary is not equivalent to no dictionary entry for display and script behavior.
-
Consider filtering empty items when listing inventories too.
- The data model should not keep
x0entries, but display code can defensively skipitem.IsEmpty.
- The data model should not keep
-
Keep the mirror sync idea, but add tests around these cases:
- Shift-click mirrored player slot into an empty container slot.
- Shift-click mirrored player slot into an existing compatible stack.
- Shift-click one item twice into the same chest stack.
- Open container
WindowItemssync followed by localDoWindowAction(). - Server
OnSetSlot(windowId, sourceSlot, null, stateId)after local prediction already changed the source item to zero.
-
Keep #3124 separate.
- Fixing inventory sync should not be expected to fix Script Scheduler duplicate script instances after reconnect.