Compare commits
8 Commits
456913cea0
...
4daed48f0e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4daed48f0e | ||
|
|
a5a1293622 | ||
|
|
ebce4e852d | ||
|
|
2c157be8cb | ||
|
|
a5bc9789a3 | ||
|
|
b32a13ab84 | ||
|
|
31fd6c33ff | ||
|
|
ce3e83b928 |
212
README.md
212
README.md
@@ -1,25 +1,199 @@
|
|||||||
|

|
||||||
|
|
||||||
Installation information
|
# Trade
|
||||||
=======
|
|
||||||
|
|
||||||
This template repository can be directly cloned to get you started with a new
|
`Trade` is a NeoForge mod for Minecraft `1.21.1` that adds a secure player-to-player trading interface inspired by Runescape.
|
||||||
mod. Simply create a new repository cloned from this one, by following the
|
|
||||||
instructions provided by [GitHub](https://docs.github.com/en/repositories/creating-and-managing-repositories/creating-a-repository-from-a-template).
|
|
||||||
|
|
||||||
Once you have your clone, simply open the repository in the IDE of your choice. The usual recommendation for an IDE is either IntelliJ IDEA or Eclipse.
|
Players can request a trade, review both offers in a shared GUI, accept once to lock the offer, and confirm a second time to complete the exchange.
|
||||||
|
|
||||||
If at any point you are missing libraries in your IDE, or you've run into problems you can
|
## Features
|
||||||
run `gradlew --refresh-dependencies` to refresh the local cache. `gradlew clean` to reset everything
|
|
||||||
{this does not affect your code} and then start the process again.
|
|
||||||
|
|
||||||
Mapping Names:
|
- Player-to-player trade requests with an accept / decline handshake
|
||||||
============
|
- Shared trade screen with separate offer areas for both players
|
||||||
By default, the MDK is configured to use the official mapping names from Mojang for methods and fields
|
- Two-step confirmation flow
|
||||||
in the Minecraft codebase. These names are covered by a specific license. All modders should be aware of this
|
- Quantity-based item selection with quick amounts and `Trade X`
|
||||||
license. For the latest license text, refer to the mapping file itself, or the reference copy here:
|
- Inventory-safe finalization: nothing changes until safety and security checks pass and the trade succeeds
|
||||||
https://github.com/NeoForged/NeoForm/blob/main/Mojang.md
|
- Configurable trade safety checks to prevent dangerous mid-combat or mid-movement trading
|
||||||
|
- Per-player trade toggles and ignore list support
|
||||||
|
- File-based trade audit log for server operators
|
||||||
|
|
||||||
Additional Resources:
|
## Player Usage
|
||||||
==========
|
|
||||||
Community Documentation: https://docs.neoforged.net/
|
### Starting a trade
|
||||||
NeoForged Discord: https://discord.neoforged.net/
|
|
||||||
|
Primary flow:
|
||||||
|
|
||||||
|
- Use the trade keybind (default: `G`) while looking at another player
|
||||||
|
|
||||||
|
Command option:
|
||||||
|
|
||||||
|
```mcfunction
|
||||||
|
/trade <player>
|
||||||
|
```
|
||||||
|
|
||||||
|
When a request is sent, the target player receives chat text with a clickable accept link.
|
||||||
|
|
||||||
|
They can also respond manually:
|
||||||
|
|
||||||
|
```mcfunction
|
||||||
|
/trade yes
|
||||||
|
/trade no
|
||||||
|
```
|
||||||
|
|
||||||
|
### In the trade screen
|
||||||
|
|
||||||
|
- Click an item in your inventory to trade `1`
|
||||||
|
- Right-click an item to open the quantity menu
|
||||||
|
- Use `Trade X` to enter a custom amount
|
||||||
|
- Click `Accept` when your offer is ready
|
||||||
|
- Once both players accept, the screen enters the confirmation stage
|
||||||
|
- Click `Confirm` to finalize
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
### Player commands
|
||||||
|
|
||||||
|
```mcfunction
|
||||||
|
/trade <player>
|
||||||
|
/trade yes
|
||||||
|
/trade no
|
||||||
|
/trade toggle
|
||||||
|
/trade on
|
||||||
|
/trade off
|
||||||
|
/trade ignore <player>
|
||||||
|
/trade unignore <player>
|
||||||
|
/trade ignorelist
|
||||||
|
```
|
||||||
|
|
||||||
|
### Command behavior
|
||||||
|
|
||||||
|
- `/trade <player>` sends a trade request
|
||||||
|
- `/trade yes` accepts the current pending trade request
|
||||||
|
- `/trade no` declines the current pending trade request
|
||||||
|
- `/trade toggle` flips whether you accept incoming trade requests
|
||||||
|
- `/trade on` enables incoming trade requests
|
||||||
|
- `/trade off` disables incoming trade requests
|
||||||
|
- `/trade ignore <player>` blocks trade requests from that online player
|
||||||
|
- `/trade unignore <player>` removes that online player from your ignore list
|
||||||
|
- `/trade ignorelist` shows your ignored players
|
||||||
|
|
||||||
|
## Trade Safety
|
||||||
|
|
||||||
|
By default, the mod blocks trade requests or acceptance unless both players are in a safe state.
|
||||||
|
|
||||||
|
Default safeguards:
|
||||||
|
|
||||||
|
- on solid ground
|
||||||
|
- standing still
|
||||||
|
- not recently damaged
|
||||||
|
- not on fire
|
||||||
|
- not in liquid
|
||||||
|
- not sleeping
|
||||||
|
- not gliding
|
||||||
|
- not mounted
|
||||||
|
- in the same dimension
|
||||||
|
|
||||||
|
There is also a special delayed-accept flow for the most common transient failure:
|
||||||
|
|
||||||
|
- if the other player accepts while you are only moving or in the air, the request does not fail immediately
|
||||||
|
- instead, you get an on-screen countdown
|
||||||
|
- if you get safe in time and remain still long enough, the trade opens automatically
|
||||||
|
|
||||||
|
## Final Validation
|
||||||
|
|
||||||
|
When both players confirm, the trade is checked one last time before any real inventory is changed.
|
||||||
|
|
||||||
|
The trade verifies:
|
||||||
|
|
||||||
|
- both players are still in the same dimension, if that config is enabled
|
||||||
|
- both players are still within configured trade distance, if enabled
|
||||||
|
- both live inventories still match the snapshots taken when the trade started
|
||||||
|
- both players can receive the incoming items
|
||||||
|
|
||||||
|
If any check fails, the trade is cancelled and both players receive an explicit chat message explaining why.
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
Server config values live under the `trade` section.
|
||||||
|
|
||||||
|
### Request / range
|
||||||
|
|
||||||
|
- `tradeCommandProximity`
|
||||||
|
- default: `0`
|
||||||
|
- `0` disables range checks
|
||||||
|
- `1+` requires players to be within that many blocks to request, accept, and finalize a trade
|
||||||
|
|
||||||
|
- `requestTimeoutSeconds`
|
||||||
|
- default: `30`
|
||||||
|
- number of seconds before a pending trade request expires
|
||||||
|
|
||||||
|
### Debug
|
||||||
|
|
||||||
|
- `enableDebugFeatures`
|
||||||
|
- default: `false`
|
||||||
|
- enables debug commands and debug UI/testing tools
|
||||||
|
|
||||||
|
### Safety
|
||||||
|
|
||||||
|
- `requireOnGround`
|
||||||
|
- default: `true`
|
||||||
|
|
||||||
|
- `requireStationary`
|
||||||
|
- default: `true`
|
||||||
|
|
||||||
|
- `stationarySpeedThreshold`
|
||||||
|
- default: `0.03`
|
||||||
|
|
||||||
|
- `requireNoRecentDamage`
|
||||||
|
- default: `true`
|
||||||
|
|
||||||
|
- `noDamageSeconds`
|
||||||
|
- default: `10`
|
||||||
|
|
||||||
|
- `requireNotOnFire`
|
||||||
|
- default: `true`
|
||||||
|
|
||||||
|
- `requireNotInLiquid`
|
||||||
|
- default: `true`
|
||||||
|
|
||||||
|
- `requireNotSleeping`
|
||||||
|
- default: `true`
|
||||||
|
|
||||||
|
- `requireNotFallFlying`
|
||||||
|
- default: `true`
|
||||||
|
|
||||||
|
- `requireNotRiding`
|
||||||
|
- default: `true`
|
||||||
|
|
||||||
|
- `requireSameDimension`
|
||||||
|
- default: `true`
|
||||||
|
|
||||||
|
## Audit Log
|
||||||
|
|
||||||
|
The mod writes a trade audit log to:
|
||||||
|
|
||||||
|
```text
|
||||||
|
logs/trade.log
|
||||||
|
```
|
||||||
|
|
||||||
|
Entries include:
|
||||||
|
|
||||||
|
- trade requests
|
||||||
|
- request accepts / declines / expirations
|
||||||
|
- trade opens
|
||||||
|
- trade cancels
|
||||||
|
- trade completions
|
||||||
|
- player names
|
||||||
|
- UUIDs
|
||||||
|
- dimensions
|
||||||
|
- block coordinates
|
||||||
|
- items offered by each side
|
||||||
|
- final result text
|
||||||
|
|
||||||
|
## Debug Mode
|
||||||
|
|
||||||
|
Debug tools are disabled by default and only available when:
|
||||||
|
|
||||||
|
- `enableDebugFeatures = true`
|
||||||
|
|
||||||
|
When enabled, the mod exposes `/trade debug ...` commands and on-screen debug controls for single-client testing.
|
||||||
BIN
screenshot1.png
Normal file
BIN
screenshot1.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 87 KiB |
@@ -1,6 +1,6 @@
|
|||||||
package com.trunksbomb.trade.mod.client;
|
package com.trunksbomb.trade.client;
|
||||||
|
|
||||||
import com.trunksbomb.trade.mod.trade.TradeView;
|
import com.trunksbomb.trade.trade.TradeView;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import net.minecraft.client.Minecraft;
|
import net.minecraft.client.Minecraft;
|
||||||
import net.minecraft.network.chat.Component;
|
import net.minecraft.network.chat.Component;
|
||||||
@@ -18,6 +18,13 @@ public final class TradeClientState {
|
|||||||
return minecraft;
|
return minecraft;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void showOrUpdate(TradeView view) {
|
||||||
|
if (minecraft == null) {
|
||||||
|
minecraft = Minecraft.getInstance();
|
||||||
|
}
|
||||||
|
showOrUpdate(minecraft, view);
|
||||||
|
}
|
||||||
|
|
||||||
public static void showOrUpdate(Minecraft minecraftInstance, TradeView view) {
|
public static void showOrUpdate(Minecraft minecraftInstance, TradeView view) {
|
||||||
minecraft = minecraftInstance;
|
minecraft = minecraftInstance;
|
||||||
if (minecraft.screen instanceof TradeScreen screen && screen.sessionId().equals(view.sessionId())) {
|
if (minecraft.screen instanceof TradeScreen screen && screen.sessionId().equals(view.sessionId())) {
|
||||||
@@ -32,14 +39,17 @@ public final class TradeClientState {
|
|||||||
minecraft.setScreen(new TradeScreen(new TradeScreen.TradeMenu(0, minecraft.player.getInventory(), view), minecraft.player.getInventory(), Component.literal("Trade")));
|
minecraft.setScreen(new TradeScreen(new TradeScreen.TradeMenu(0, minecraft.player.getInventory(), view), minecraft.player.getInventory(), Component.literal("Trade")));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void closeTrade(UUID sessionId, Component reason) {
|
||||||
|
if (minecraft == null) {
|
||||||
|
minecraft = Minecraft.getInstance();
|
||||||
|
}
|
||||||
|
closeTrade(minecraft, sessionId, reason);
|
||||||
|
}
|
||||||
|
|
||||||
public static void closeTrade(Minecraft minecraftInstance, UUID sessionId, Component reason) {
|
public static void closeTrade(Minecraft minecraftInstance, UUID sessionId, Component reason) {
|
||||||
minecraft = minecraftInstance;
|
minecraft = minecraftInstance;
|
||||||
if (minecraft.screen instanceof TradeScreen screen && screen.sessionId().equals(sessionId)) {
|
if (minecraft.screen instanceof TradeScreen screen && screen.sessionId().equals(sessionId)) {
|
||||||
minecraft.setScreen(null);
|
minecraft.setScreen(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (minecraft.player != null) {
|
|
||||||
minecraft.player.displayClientMessage(reason, false);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,18 @@
|
|||||||
package com.trunksbomb.trade.mod.client;
|
package com.trunksbomb.trade.client;
|
||||||
|
|
||||||
import com.trunksbomb.trade.mod.network.TradeActionPayload;
|
import com.trunksbomb.trade.network.TradeActionPayload;
|
||||||
import com.trunksbomb.trade.mod.network.DebugTradeControlPayload;
|
import com.trunksbomb.trade.network.DebugTradeControlPayload;
|
||||||
import com.trunksbomb.trade.mod.trade.DebugControlAction;
|
import com.trunksbomb.trade.trade.DebugControlAction;
|
||||||
import com.trunksbomb.trade.mod.trade.TradeAction;
|
import com.trunksbomb.trade.trade.DebugUnsafeState;
|
||||||
import com.trunksbomb.trade.mod.trade.TradeStage;
|
import com.trunksbomb.trade.trade.TradeAction;
|
||||||
import com.trunksbomb.trade.mod.trade.TradeView;
|
import com.trunksbomb.trade.trade.TradeStage;
|
||||||
|
import com.trunksbomb.trade.trade.TradeView;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import net.minecraft.client.gui.GuiGraphics;
|
import net.minecraft.client.gui.GuiGraphics;
|
||||||
import net.minecraft.client.gui.components.Button;
|
import net.minecraft.client.gui.components.Button;
|
||||||
|
import net.minecraft.client.gui.components.EditBox;
|
||||||
import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen;
|
import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen;
|
||||||
import net.minecraft.network.chat.Component;
|
import net.minecraft.network.chat.Component;
|
||||||
import net.minecraft.resources.ResourceLocation;
|
import net.minecraft.resources.ResourceLocation;
|
||||||
@@ -30,8 +34,11 @@ public class TradeScreen extends AbstractContainerScreen<TradeScreen.TradeMenu>
|
|||||||
private static final int OFFER_START_Y = 18;
|
private static final int OFFER_START_Y = 18;
|
||||||
private static final int INVENTORY_START_X = 62;
|
private static final int INVENTORY_START_X = 62;
|
||||||
private static final int INVENTORY_START_Y = 139;
|
private static final int INVENTORY_START_Y = 139;
|
||||||
|
private static final int BANNER_X = 58;
|
||||||
|
private static final int BANNER_Y = 6;
|
||||||
private static final int CENTER_COLUMN_X = 115;
|
private static final int CENTER_COLUMN_X = 115;
|
||||||
private static final int STATUS_LABEL_Y = 54;
|
private static final int STATUS_LABEL_Y = 54;
|
||||||
|
private static final int CONFIRM_LABEL_Y = 34;
|
||||||
private static final int ACCEPT_BUTTON_Y = 74;
|
private static final int ACCEPT_BUTTON_Y = 74;
|
||||||
private static final int CANCEL_BUTTON_Y = 98;
|
private static final int CANCEL_BUTTON_Y = 98;
|
||||||
private static final int ACTION_BUTTON_WIDTH = 48;
|
private static final int ACTION_BUTTON_WIDTH = 48;
|
||||||
@@ -40,10 +47,13 @@ public class TradeScreen extends AbstractContainerScreen<TradeScreen.TradeMenu>
|
|||||||
private static final int DEBUG_BUTTON_WIDTH = 86;
|
private static final int DEBUG_BUTTON_WIDTH = 86;
|
||||||
private static final int DEBUG_BUTTON_HEIGHT = 20;
|
private static final int DEBUG_BUTTON_HEIGHT = 20;
|
||||||
private static final int DEBUG_BUTTON_GAP = 4;
|
private static final int DEBUG_BUTTON_GAP = 4;
|
||||||
private static final int CONFIRM_PANEL_X = 89;
|
private static final int DEBUG_SMALL_BUTTON_WIDTH = 41;
|
||||||
private static final int CONFIRM_PANEL_Y = 77;
|
private static final int AMOUNT_PROMPT_WIDTH = 90;
|
||||||
private static final int CONFIRM_PANEL_WIDTH = 106;
|
private static final int AMOUNT_PROMPT_HEIGHT = 44;
|
||||||
private static final int CONFIRM_PANEL_HEIGHT = 64;
|
private ContextMenu contextMenu;
|
||||||
|
private EditBox amountInput;
|
||||||
|
private AmountPrompt amountPrompt;
|
||||||
|
private Button acceptButton;
|
||||||
|
|
||||||
public TradeScreen(TradeMenu menu, Inventory inventory, Component title) {
|
public TradeScreen(TradeMenu menu, Inventory inventory, Component title) {
|
||||||
super(menu, inventory, title);
|
super(menu, inventory, title);
|
||||||
@@ -64,13 +74,19 @@ public class TradeScreen extends AbstractContainerScreen<TradeScreen.TradeMenu>
|
|||||||
@Override
|
@Override
|
||||||
protected void init() {
|
protected void init() {
|
||||||
super.init();
|
super.init();
|
||||||
titleLabelX = LEFT_OFFER_START_X;
|
titleLabelX = BANNER_X;
|
||||||
titleLabelY = 6;
|
titleLabelY = BANNER_Y;
|
||||||
|
amountInput = new EditBox(font, leftPos + 0, topPos + 0, 54, 14, Component.literal("Trade Amount"));
|
||||||
|
amountInput.setVisible(false);
|
||||||
|
amountInput.setCanLoseFocus(false);
|
||||||
|
amountInput.setMaxLength(3);
|
||||||
|
amountInput.setFilter(value -> value.isEmpty() || value.chars().allMatch(Character::isDigit));
|
||||||
|
addRenderableWidget(amountInput);
|
||||||
|
|
||||||
addRenderableWidget(Button.builder(Component.literal("Accept"), button -> sendAction(TradeAction.ACCEPT, -1, false))
|
acceptButton = addRenderableWidget(Button.builder(acceptButtonLabel(), button -> sendAction(TradeAction.ACCEPT, -1, 1))
|
||||||
.bounds(leftPos + CENTER_COLUMN_X + 3, topPos + ACCEPT_BUTTON_Y, ACTION_BUTTON_WIDTH, ACTION_BUTTON_HEIGHT)
|
.bounds(leftPos + CENTER_COLUMN_X + 3, topPos + ACCEPT_BUTTON_Y, ACTION_BUTTON_WIDTH, ACTION_BUTTON_HEIGHT)
|
||||||
.build());
|
.build());
|
||||||
addRenderableWidget(Button.builder(Component.literal("Cancel"), button -> sendAction(TradeAction.DECLINE, -1, false))
|
addRenderableWidget(Button.builder(Component.literal("Cancel"), button -> sendAction(TradeAction.DECLINE, -1, 1))
|
||||||
.bounds(leftPos + CENTER_COLUMN_X + 3, topPos + CANCEL_BUTTON_Y, ACTION_BUTTON_WIDTH, ACTION_BUTTON_HEIGHT)
|
.bounds(leftPos + CENTER_COLUMN_X + 3, topPos + CANCEL_BUTTON_Y, ACTION_BUTTON_WIDTH, ACTION_BUTTON_HEIGHT)
|
||||||
.build());
|
.build());
|
||||||
|
|
||||||
@@ -89,21 +105,49 @@ public class TradeScreen extends AbstractContainerScreen<TradeScreen.TradeMenu>
|
|||||||
addRenderableWidget(Button.builder(Component.literal("Remove Item"), button -> sendDebug(DebugControlAction.REMOVE_LAST))
|
addRenderableWidget(Button.builder(Component.literal("Remove Item"), button -> sendDebug(DebugControlAction.REMOVE_LAST))
|
||||||
.bounds(debugX, debugY + 3 * (DEBUG_BUTTON_HEIGHT + DEBUG_BUTTON_GAP), DEBUG_BUTTON_WIDTH, DEBUG_BUTTON_HEIGHT)
|
.bounds(debugX, debugY + 3 * (DEBUG_BUTTON_HEIGHT + DEBUG_BUTTON_GAP), DEBUG_BUTTON_WIDTH, DEBUG_BUTTON_HEIGHT)
|
||||||
.build());
|
.build());
|
||||||
|
|
||||||
|
int unsafeY = debugY + 4 * (DEBUG_BUTTON_HEIGHT + DEBUG_BUTTON_GAP) + 6;
|
||||||
|
addRenderableWidget(debugUnsafeButton("Damage", debugX, unsafeY, DebugUnsafeState.DAMAGE));
|
||||||
|
addRenderableWidget(debugUnsafeButton("Fire", debugX + DEBUG_SMALL_BUTTON_WIDTH + 4, unsafeY, DebugUnsafeState.FIRE));
|
||||||
|
addRenderableWidget(debugUnsafeButton("Liquid", debugX, unsafeY + DEBUG_BUTTON_HEIGHT + DEBUG_BUTTON_GAP, DebugUnsafeState.LIQUID));
|
||||||
|
addRenderableWidget(debugUnsafeButton("Moving", debugX + DEBUG_SMALL_BUTTON_WIDTH + 4, unsafeY + DEBUG_BUTTON_HEIGHT + DEBUG_BUTTON_GAP, DebugUnsafeState.MOVING));
|
||||||
|
addRenderableWidget(debugUnsafeButton("Sleep", debugX, unsafeY + 2 * (DEBUG_BUTTON_HEIGHT + DEBUG_BUTTON_GAP), DebugUnsafeState.SLEEPING));
|
||||||
|
addRenderableWidget(debugUnsafeButton("Ride", debugX + DEBUG_SMALL_BUTTON_WIDTH + 4, unsafeY + 2 * (DEBUG_BUTTON_HEIGHT + DEBUG_BUTTON_GAP), DebugUnsafeState.RIDING));
|
||||||
|
addRenderableWidget(debugUnsafeButton("Glide", debugX, unsafeY + 3 * (DEBUG_BUTTON_HEIGHT + DEBUG_BUTTON_GAP), DebugUnsafeState.GLIDING));
|
||||||
|
addRenderableWidget(Button.builder(Component.literal("Clear"), button -> sendDebug(DebugControlAction.CLEAR_UNSAFE))
|
||||||
|
.bounds(debugX + DEBUG_SMALL_BUTTON_WIDTH + 4, unsafeY + 3 * (DEBUG_BUTTON_HEIGHT + DEBUG_BUTTON_GAP), DEBUG_SMALL_BUTTON_WIDTH, DEBUG_BUTTON_HEIGHT)
|
||||||
|
.build());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean keyPressed(int keyCode, int scanCode, int modifiers) {
|
public boolean keyPressed(int keyCode, int scanCode, int modifiers) {
|
||||||
|
if (amountPrompt != null) {
|
||||||
|
if (keyCode == 256) {
|
||||||
|
closeAmountPrompt();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (keyCode == 257 || keyCode == 335) {
|
||||||
|
submitAmountPrompt();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (amountInput != null && amountInput.keyPressed(keyCode, scanCode, modifiers)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
if (minecraft != null && minecraft.options.keyInventory.matches(keyCode, scanCode)) {
|
if (minecraft != null && minecraft.options.keyInventory.matches(keyCode, scanCode)) {
|
||||||
onClose();
|
onClose();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
if (hoveredSlot != null) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
return super.keyPressed(keyCode, scanCode, modifiers);
|
return super.keyPressed(keyCode, scanCode, modifiers);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onClose() {
|
public void onClose() {
|
||||||
sendAction(TradeAction.CLOSE, -1, false);
|
sendAction(TradeAction.CLOSE, -1, 1);
|
||||||
if (minecraft != null) {
|
if (minecraft != null) {
|
||||||
minecraft.setScreen(null);
|
minecraft.setScreen(null);
|
||||||
}
|
}
|
||||||
@@ -111,12 +155,37 @@ public class TradeScreen extends AbstractContainerScreen<TradeScreen.TradeMenu>
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean mouseClicked(double mouseX, double mouseY, int button) {
|
public boolean mouseClicked(double mouseX, double mouseY, int button) {
|
||||||
|
if (amountPrompt != null) {
|
||||||
|
if (isInsideAmountPrompt(mouseX, mouseY)) {
|
||||||
|
if (amountInput != null && amountInput.mouseClicked(mouseX, mouseY, button)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
closeAmountPrompt();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (contextMenu != null) {
|
||||||
|
if (handleContextMenuClick(mouseX, mouseY)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
contextMenu = null;
|
||||||
|
}
|
||||||
|
|
||||||
if (menu.view().stage() == TradeStage.CONFIRMING) {
|
if (menu.view().stage() == TradeStage.CONFIRMING) {
|
||||||
|
if (hoveredSlot != null) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
return super.mouseClicked(mouseX, mouseY, button);
|
return super.mouseClicked(mouseX, mouseY, button);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hoveredSlot instanceof SelfOfferSlot selfOfferSlot) {
|
if (hoveredSlot instanceof SelfOfferSlot selfOfferSlot) {
|
||||||
sendAction(TradeAction.REMOVE_ITEM, selfOfferSlot.offerIndex(), button == 1);
|
if (button == 1) {
|
||||||
|
openContextMenu(mouseX, mouseY, TradeAction.REMOVE_ITEM, selfOfferSlot.offerIndex(), hoveredSlot.getItem(), "Remove");
|
||||||
|
} else {
|
||||||
|
sendAction(TradeAction.REMOVE_ITEM, selfOfferSlot.offerIndex(), 1);
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -124,8 +193,16 @@ public class TradeScreen extends AbstractContainerScreen<TradeScreen.TradeMenu>
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hoveredSlot != null && hoveredSlot.container == menu.playerInventory()) {
|
if (hoveredSlot instanceof GhostInventorySlot ghostInventorySlot) {
|
||||||
sendAction(TradeAction.ADD_ITEM, hoveredSlot.getSlotIndex(), button == 1);
|
if (button == 1) {
|
||||||
|
openContextMenu(mouseX, mouseY, TradeAction.ADD_ITEM, ghostInventorySlot.inventoryIndex(), hoveredSlot.getItem(), "Trade");
|
||||||
|
} else {
|
||||||
|
sendAction(TradeAction.ADD_ITEM, ghostInventorySlot.inventoryIndex(), 1);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hoveredSlot != null) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -135,55 +212,70 @@ public class TradeScreen extends AbstractContainerScreen<TradeScreen.TradeMenu>
|
|||||||
@Override
|
@Override
|
||||||
protected void renderBg(GuiGraphics guiGraphics, float partialTick, int mouseX, int mouseY) {
|
protected void renderBg(GuiGraphics guiGraphics, float partialTick, int mouseX, int mouseY) {
|
||||||
guiGraphics.blit(TEXTURE, leftPos, topPos, 0, 0, imageWidth, imageHeight, imageWidth, imageHeight);
|
guiGraphics.blit(TEXTURE, leftPos, topPos, 0, 0, imageWidth, imageHeight, imageWidth, imageHeight);
|
||||||
if (menu.view().stage() == TradeStage.CONFIRMING) {
|
|
||||||
renderConfirmationOverlay(guiGraphics);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void renderLabels(GuiGraphics guiGraphics, int mouseX, int mouseY) {
|
protected void renderLabels(GuiGraphics guiGraphics, int mouseX, int mouseY) {
|
||||||
guiGraphics.drawString(font, menu.view().selfName(), titleLabelX, titleLabelY, 0x404040, false);
|
guiGraphics.drawString(font, "Trading with " + menu.view().otherName(), titleLabelX, titleLabelY, 0x404040, false);
|
||||||
guiGraphics.drawString(font, menu.view().otherName(), RIGHT_OFFER_START_X, 6, 0x404040, false);
|
|
||||||
guiGraphics.drawString(font, playerInventoryTitle, inventoryLabelX, inventoryLabelY, 0x404040, false);
|
guiGraphics.drawString(font, playerInventoryTitle, inventoryLabelX, inventoryLabelY, 0x404040, false);
|
||||||
|
if (menu.view().stage() == TradeStage.CONFIRMING) {
|
||||||
|
drawScaledCenteredColumnText(guiGraphics, "Are you", CONFIRM_LABEL_Y, 0xB02020, 0.7F);
|
||||||
|
drawScaledCenteredColumnText(guiGraphics, "sure you want", CONFIRM_LABEL_Y + 7, 0xB02020, 0.7F);
|
||||||
|
drawScaledCenteredColumnText(guiGraphics, "to trade?", CONFIRM_LABEL_Y + 14, 0xB02020, 0.7F);
|
||||||
|
}
|
||||||
if (menu.view().otherAccepted()) {
|
if (menu.view().otherAccepted()) {
|
||||||
drawScaledCenteredColumnText(guiGraphics, "Other player", STATUS_LABEL_Y, 0x2E7D32, 0.7F);
|
drawScaledCenteredColumnText(guiGraphics, "Other player", STATUS_LABEL_Y, 0x2E7D32, 0.7F);
|
||||||
drawScaledCenteredColumnText(guiGraphics, "has accepted", STATUS_LABEL_Y + 7, 0x2E7D32, 0.7F);
|
drawScaledCenteredColumnText(guiGraphics, "has accepted", STATUS_LABEL_Y + 7, 0x2E7D32, 0.7F);
|
||||||
}
|
} else if (menu.view().selfAccepted()) {
|
||||||
if (menu.view().stage() == TradeStage.CONFIRMING) {
|
if (menu.view().stage() == TradeStage.CONFIRMING) {
|
||||||
drawCenteredColumnText(guiGraphics, "FINAL", 16, 0x7A4F00);
|
drawScaledCenteredColumnText(guiGraphics, "Waiting for", STATUS_LABEL_Y, 0x2E7D32, 0.7F);
|
||||||
drawCenteredColumnText(guiGraphics, "CONFIRM", 28, 0x7A4F00);
|
drawScaledCenteredColumnText(guiGraphics, "other player", STATUS_LABEL_Y + 7, 0x2E7D32, 0.7F);
|
||||||
|
drawScaledCenteredColumnText(guiGraphics, "to confirm", STATUS_LABEL_Y + 14, 0x2E7D32, 0.7F);
|
||||||
|
} else {
|
||||||
|
drawScaledCenteredColumnText(guiGraphics, "Waiting for", STATUS_LABEL_Y, 0x2E7D32, 0.7F);
|
||||||
|
drawScaledCenteredColumnText(guiGraphics, "other player", STATUS_LABEL_Y + 7, 0x2E7D32, 0.7F);
|
||||||
|
drawScaledCenteredColumnText(guiGraphics, "to accept", STATUS_LABEL_Y + 14, 0x2E7D32, 0.7F);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void render(GuiGraphics guiGraphics, int mouseX, int mouseY, float partialTick) {
|
public void render(GuiGraphics guiGraphics, int mouseX, int mouseY, float partialTick) {
|
||||||
|
if (acceptButton != null) {
|
||||||
|
acceptButton.setMessage(acceptButtonLabel());
|
||||||
|
}
|
||||||
super.render(guiGraphics, mouseX, mouseY, partialTick);
|
super.render(guiGraphics, mouseX, mouseY, partialTick);
|
||||||
|
renderContextMenu(guiGraphics, mouseX, mouseY);
|
||||||
|
renderAmountPrompt(guiGraphics);
|
||||||
renderTooltip(guiGraphics, mouseX, mouseY);
|
renderTooltip(guiGraphics, mouseX, mouseY);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void sendAction(TradeAction action, int slot, boolean secondary) {
|
private Component acceptButtonLabel() {
|
||||||
|
return Component.literal(menu.view().stage() == TradeStage.CONFIRMING ? "Confirm" : "Accept");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void sendAction(TradeAction action, int slot, int amount) {
|
||||||
if (minecraft == null || minecraft.getConnection() == null) {
|
if (minecraft == null || minecraft.getConnection() == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
PacketDistributor.sendToServer(new TradeActionPayload(menu.view().sessionId(), action, slot, secondary));
|
PacketDistributor.sendToServer(new TradeActionPayload(menu.view().sessionId(), action, slot, amount));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void sendDebug(DebugControlAction action) {
|
private void sendDebug(DebugControlAction action) {
|
||||||
|
sendDebug(action, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void sendDebug(DebugControlAction action, String spec) {
|
||||||
if (minecraft == null || minecraft.getConnection() == null) {
|
if (minecraft == null || minecraft.getConnection() == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
PacketDistributor.sendToServer(new DebugTradeControlPayload(menu.view().sessionId(), action, ""));
|
PacketDistributor.sendToServer(new DebugTradeControlPayload(menu.view().sessionId(), action, spec));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void renderConfirmationOverlay(GuiGraphics guiGraphics) {
|
private Button debugUnsafeButton(String label, int x, int y, DebugUnsafeState state) {
|
||||||
int panelLeft = leftPos + CONFIRM_PANEL_X;
|
return Button.builder(Component.literal(label), button -> sendDebug(DebugControlAction.SET_UNSAFE, state.name()))
|
||||||
int panelTop = topPos + CONFIRM_PANEL_Y;
|
.bounds(x, y, DEBUG_SMALL_BUTTON_WIDTH, DEBUG_BUTTON_HEIGHT)
|
||||||
guiGraphics.fill(leftPos, topPos, leftPos + imageWidth, topPos + imageHeight, 0x22000000);
|
.build();
|
||||||
guiGraphics.fill(panelLeft, panelTop, panelLeft + CONFIRM_PANEL_WIDTH, panelTop + CONFIRM_PANEL_HEIGHT, 0xDDF7D48C);
|
|
||||||
guiGraphics.fill(panelLeft + 1, panelTop + 1, panelLeft + CONFIRM_PANEL_WIDTH - 1, panelTop + CONFIRM_PANEL_HEIGHT - 1, 0xAAFFF4D6);
|
|
||||||
guiGraphics.drawString(font, Component.literal("Confirm Trade"), panelLeft + 11, panelTop + 9, 0x5B3A00, false);
|
|
||||||
guiGraphics.drawString(font, Component.literal("Review items"), panelLeft + 18, panelTop + 24, 0x5B3A00, false);
|
|
||||||
guiGraphics.drawString(font, Component.literal("No more changes"), panelLeft + 9, panelTop + 36, 0x5B3A00, false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void drawCenteredColumnText(GuiGraphics guiGraphics, String text, int y, int color) {
|
private void drawCenteredColumnText(GuiGraphics guiGraphics, String text, int y, int color) {
|
||||||
@@ -202,19 +294,205 @@ public class TradeScreen extends AbstractContainerScreen<TradeScreen.TradeMenu>
|
|||||||
pose.popPose();
|
pose.popPose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void openContextMenu(double mouseX, double mouseY, TradeAction action, int slot, ItemStack stack, String verb) {
|
||||||
|
if (stack.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<ContextOption> options = new ArrayList<>();
|
||||||
|
for (int amount : contextAmounts(stack.getMaxStackSize())) {
|
||||||
|
options.add(new ContextOption(verb + " " + amount, amount));
|
||||||
|
}
|
||||||
|
options.add(new ContextOption(verb + " X", -1));
|
||||||
|
contextMenu = new ContextMenu(action, slot, options, (int) mouseX, (int) mouseY);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean handleContextMenuClick(double mouseX, double mouseY) {
|
||||||
|
if (contextMenu == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
int menuX = contextMenuX();
|
||||||
|
int menuY = contextMenuY();
|
||||||
|
int menuWidth = contextMenuWidth();
|
||||||
|
int optionHeight = 12;
|
||||||
|
int menuHeight = contextMenu.options().size() * optionHeight + 4;
|
||||||
|
if (mouseX < menuX || mouseX >= menuX + menuWidth || mouseY < menuY || mouseY >= menuY + menuHeight) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
int index = ((int) mouseY - menuY - 2) / optionHeight;
|
||||||
|
if (index < 0 || index >= contextMenu.options().size()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
ContextOption option = contextMenu.options().get(index);
|
||||||
|
if (option.amount() < 0) {
|
||||||
|
openAmountPrompt(contextMenu.action(), contextMenu.slot(), contextMenu.mouseX(), contextMenu.mouseY());
|
||||||
|
} else {
|
||||||
|
sendAction(contextMenu.action(), contextMenu.slot(), option.amount());
|
||||||
|
}
|
||||||
|
|
||||||
|
contextMenu = null;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void renderContextMenu(GuiGraphics guiGraphics, int mouseX, int mouseY) {
|
||||||
|
if (contextMenu == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var pose = guiGraphics.pose();
|
||||||
|
pose.pushPose();
|
||||||
|
pose.translate(0.0F, 0.0F, 400.0F);
|
||||||
|
|
||||||
|
int menuX = contextMenuX();
|
||||||
|
int menuY = contextMenuY();
|
||||||
|
int menuWidth = contextMenuWidth();
|
||||||
|
int optionHeight = 12;
|
||||||
|
int menuHeight = contextMenu.options().size() * optionHeight + 4;
|
||||||
|
guiGraphics.fill(menuX, menuY, menuX + menuWidth, menuY + menuHeight, 0xF0101010);
|
||||||
|
guiGraphics.fill(menuX + 1, menuY + 1, menuX + menuWidth - 1, menuY + menuHeight - 1, 0xFF2B2B2B);
|
||||||
|
|
||||||
|
for (int i = 0; i < contextMenu.options().size(); i++) {
|
||||||
|
int optionY = menuY + 2 + i * optionHeight;
|
||||||
|
boolean hovered = mouseX >= menuX + 2 && mouseX < menuX + menuWidth - 2 && mouseY >= optionY && mouseY < optionY + optionHeight;
|
||||||
|
if (hovered) {
|
||||||
|
guiGraphics.fill(menuX + 2, optionY, menuX + menuWidth - 2, optionY + optionHeight, 0xFF5B6EE1);
|
||||||
|
}
|
||||||
|
guiGraphics.drawString(font, contextMenu.options().get(i).label(), menuX + 6, optionY + 2, 0xFFFFFF, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
pose.popPose();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void openAmountPrompt(TradeAction action, int slot, int mouseX, int mouseY) {
|
||||||
|
amountPrompt = new AmountPrompt(action, slot, mouseX, mouseY);
|
||||||
|
if (amountInput != null) {
|
||||||
|
amountInput.setValue("");
|
||||||
|
amountInput.setFocused(true);
|
||||||
|
amountInput.setVisible(true);
|
||||||
|
setFocused(amountInput);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void submitAmountPrompt() {
|
||||||
|
if (amountPrompt == null || amountInput == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String value = amountInput.getValue().trim();
|
||||||
|
if (value.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
int amount = Integer.parseInt(value);
|
||||||
|
if (amount > 0) {
|
||||||
|
sendAction(amountPrompt.action(), amountPrompt.slot(), amount);
|
||||||
|
closeAmountPrompt();
|
||||||
|
}
|
||||||
|
} catch (NumberFormatException ignored) {
|
||||||
|
// Filter should prevent this, but ignore invalid input safely.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void closeAmountPrompt() {
|
||||||
|
amountPrompt = null;
|
||||||
|
if (amountInput != null) {
|
||||||
|
amountInput.setValue("");
|
||||||
|
amountInput.setFocused(false);
|
||||||
|
amountInput.setVisible(false);
|
||||||
|
}
|
||||||
|
setFocused(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void renderAmountPrompt(GuiGraphics guiGraphics) {
|
||||||
|
if (amountPrompt == null || amountInput == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var pose = guiGraphics.pose();
|
||||||
|
pose.pushPose();
|
||||||
|
pose.translate(0.0F, 0.0F, 450.0F);
|
||||||
|
|
||||||
|
int promptX = amountPromptX();
|
||||||
|
int promptY = amountPromptY();
|
||||||
|
guiGraphics.fill(promptX, promptY, promptX + AMOUNT_PROMPT_WIDTH, promptY + AMOUNT_PROMPT_HEIGHT, 0xF0101010);
|
||||||
|
guiGraphics.fill(promptX + 1, promptY + 1, promptX + AMOUNT_PROMPT_WIDTH - 1, promptY + AMOUNT_PROMPT_HEIGHT - 1, 0xFF2B2B2B);
|
||||||
|
guiGraphics.drawString(font, amountPrompt.action() == TradeAction.ADD_ITEM ? "Trade amount" : "Remove amount", promptX + 6, promptY + 6, 0xFFFFFF, false);
|
||||||
|
guiGraphics.drawString(font, "Enter to confirm", promptX + 6, promptY + 30, 0xAAAAAA, false);
|
||||||
|
|
||||||
|
amountInput.setPosition(promptX + 6, promptY + 16);
|
||||||
|
amountInput.render(guiGraphics, 0, 0, 0.0F);
|
||||||
|
pose.popPose();
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isInsideAmountPrompt(double mouseX, double mouseY) {
|
||||||
|
int promptX = amountPromptX();
|
||||||
|
int promptY = amountPromptY();
|
||||||
|
return mouseX >= promptX && mouseX < promptX + AMOUNT_PROMPT_WIDTH && mouseY >= promptY && mouseY < promptY + AMOUNT_PROMPT_HEIGHT;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int amountPromptX() {
|
||||||
|
int preferredX = amountPrompt == null ? leftPos + 8 : Math.min(amountPrompt.mouseX(), this.width - AMOUNT_PROMPT_WIDTH - 4);
|
||||||
|
return Math.max(4, preferredX);
|
||||||
|
}
|
||||||
|
|
||||||
|
private int amountPromptY() {
|
||||||
|
int preferredY = amountPrompt == null ? topPos + 8 : Math.min(amountPrompt.mouseY(), this.height - AMOUNT_PROMPT_HEIGHT - 4);
|
||||||
|
return Math.max(4, preferredY);
|
||||||
|
}
|
||||||
|
|
||||||
|
private int contextMenuX() {
|
||||||
|
int width = contextMenuWidth();
|
||||||
|
return Math.min(contextMenu.mouseX(), this.width - width - 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
private int contextMenuY() {
|
||||||
|
int height = contextMenu.options().size() * 12 + 4;
|
||||||
|
return Math.min(contextMenu.mouseY(), this.height - height - 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
private int contextMenuWidth() {
|
||||||
|
int width = 0;
|
||||||
|
for (ContextOption option : contextMenu.options()) {
|
||||||
|
width = Math.max(width, font.width(option.label()));
|
||||||
|
}
|
||||||
|
return width + 12;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int[] contextAmounts(int maxStackSize) {
|
||||||
|
if (maxStackSize == 1) {
|
||||||
|
return new int[] {1};
|
||||||
|
}
|
||||||
|
if (maxStackSize == 16) {
|
||||||
|
return new int[] {1, 2, 4, 8, 16};
|
||||||
|
}
|
||||||
|
if (maxStackSize == 32) {
|
||||||
|
return new int[] {1, 4, 8, 16, 32};
|
||||||
|
}
|
||||||
|
return new int[] {1, 8, 16, 32, 64};
|
||||||
|
}
|
||||||
|
|
||||||
|
private record ContextMenu(TradeAction action, int slot, List<ContextOption> options, int mouseX, int mouseY) {}
|
||||||
|
|
||||||
|
private record ContextOption(String label, int amount) {}
|
||||||
|
|
||||||
|
private record AmountPrompt(TradeAction action, int slot, int mouseX, int mouseY) {}
|
||||||
|
|
||||||
public static class TradeMenu extends AbstractContainerMenu {
|
public static class TradeMenu extends AbstractContainerMenu {
|
||||||
private static final int OFFER_COLUMNS = 6;
|
private static final int OFFER_COLUMNS = 6;
|
||||||
private static final int OFFER_ROWS = 6;
|
private static final int OFFER_ROWS = 6;
|
||||||
private TradeView view;
|
private TradeView view;
|
||||||
private final Inventory playerInventory;
|
private final SimpleContainer inventoryDisplay = new SimpleContainer(TradeView.INVENTORY_SLOT_COUNT);
|
||||||
private final SimpleContainer selfOffer = new SimpleContainer(TradeView.OFFER_SLOT_COUNT);
|
private final SimpleContainer selfOffer = new SimpleContainer(TradeView.OFFER_SLOT_COUNT);
|
||||||
private final SimpleContainer otherOffer = new SimpleContainer(TradeView.OFFER_SLOT_COUNT);
|
private final SimpleContainer otherOffer = new SimpleContainer(TradeView.OFFER_SLOT_COUNT);
|
||||||
|
|
||||||
public TradeMenu(int containerId, Inventory inventory, TradeView view) {
|
public TradeMenu(int containerId, Inventory inventory, TradeView view) {
|
||||||
super(MenuType.GENERIC_9x6, containerId);
|
super(MenuType.GENERIC_9x6, containerId);
|
||||||
this.playerInventory = inventory;
|
|
||||||
this.view = view;
|
this.view = view;
|
||||||
syncOfferContainers();
|
syncContainers();
|
||||||
|
|
||||||
for (int row = 0; row < OFFER_ROWS; row++) {
|
for (int row = 0; row < OFFER_ROWS; row++) {
|
||||||
for (int col = 0; col < OFFER_COLUMNS; col++) {
|
for (int col = 0; col < OFFER_COLUMNS; col++) {
|
||||||
@@ -233,12 +511,12 @@ public class TradeScreen extends AbstractContainerScreen<TradeScreen.TradeMenu>
|
|||||||
for (int row = 0; row < 3; row++) {
|
for (int row = 0; row < 3; row++) {
|
||||||
for (int col = 0; col < 9; col++) {
|
for (int col = 0; col < 9; col++) {
|
||||||
int slot = col + row * 9 + 9;
|
int slot = col + row * 9 + 9;
|
||||||
addSlot(new Slot(inventory, slot, INVENTORY_START_X + col * 18, INVENTORY_START_Y + row * 18));
|
addSlot(new GhostInventorySlot(inventoryDisplay, slot, INVENTORY_START_X + col * 18, INVENTORY_START_Y + row * 18));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (int col = 0; col < 9; col++) {
|
for (int col = 0; col < 9; col++) {
|
||||||
addSlot(new Slot(inventory, col, INVENTORY_START_X + col * 18, INVENTORY_START_Y + 58));
|
addSlot(new GhostInventorySlot(inventoryDisplay, col, INVENTORY_START_X + col * 18, INVENTORY_START_Y + 58));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -246,16 +524,15 @@ public class TradeScreen extends AbstractContainerScreen<TradeScreen.TradeMenu>
|
|||||||
return view;
|
return view;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Inventory playerInventory() {
|
|
||||||
return playerInventory;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void updateView(TradeView view) {
|
public void updateView(TradeView view) {
|
||||||
this.view = view;
|
this.view = view;
|
||||||
syncOfferContainers();
|
syncContainers();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void syncOfferContainers() {
|
private void syncContainers() {
|
||||||
|
for (int i = 0; i < TradeView.INVENTORY_SLOT_COUNT; i++) {
|
||||||
|
inventoryDisplay.setItem(i, view.inventory().get(i).copy());
|
||||||
|
}
|
||||||
for (int i = 0; i < TradeView.OFFER_SLOT_COUNT; i++) {
|
for (int i = 0; i < TradeView.OFFER_SLOT_COUNT; i++) {
|
||||||
selfOffer.setItem(i, view.selfOffer().get(i).copy());
|
selfOffer.setItem(i, view.selfOffer().get(i).copy());
|
||||||
otherOffer.setItem(i, view.otherOffer().get(i).copy());
|
otherOffer.setItem(i, view.otherOffer().get(i).copy());
|
||||||
@@ -308,4 +585,24 @@ public class TradeScreen extends AbstractContainerScreen<TradeScreen.TradeMenu>
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static class GhostInventorySlot extends Slot {
|
||||||
|
public GhostInventorySlot(Container container, int slot, int x, int y) {
|
||||||
|
super(container, slot, x, y);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int inventoryIndex() {
|
||||||
|
return getSlotIndex();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean mayPickup(Player player) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean mayPlace(ItemStack stack) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
package com.trunksbomb.trade.mod.command;
|
package com.trunksbomb.trade.command;
|
||||||
|
|
||||||
import com.mojang.brigadier.CommandDispatcher;
|
import com.mojang.brigadier.CommandDispatcher;
|
||||||
|
import com.mojang.brigadier.arguments.IntegerArgumentType;
|
||||||
import com.mojang.brigadier.arguments.StringArgumentType;
|
import com.mojang.brigadier.arguments.StringArgumentType;
|
||||||
import com.mojang.brigadier.suggestion.SuggestionsBuilder;
|
import com.mojang.brigadier.suggestion.SuggestionsBuilder;
|
||||||
import com.trunksbomb.trade.mod.trade.DebugTradeSession;
|
import com.trunksbomb.trade.mod.TradeConfig;
|
||||||
import com.trunksbomb.trade.mod.trade.TradeManager;
|
import com.trunksbomb.trade.trade.DebugUnsafeState;
|
||||||
|
import com.trunksbomb.trade.trade.DebugTradeSession;
|
||||||
|
import com.trunksbomb.trade.trade.TradeManager;
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
import net.minecraft.commands.CommandSourceStack;
|
import net.minecraft.commands.CommandSourceStack;
|
||||||
import net.minecraft.commands.Commands;
|
import net.minecraft.commands.Commands;
|
||||||
@@ -15,9 +18,17 @@ public final class TradeCommand {
|
|||||||
private TradeCommand() {}
|
private TradeCommand() {}
|
||||||
|
|
||||||
public static void register(CommandDispatcher<CommandSourceStack> dispatcher) {
|
public static void register(CommandDispatcher<CommandSourceStack> dispatcher) {
|
||||||
dispatcher.register(Commands.literal("trade")
|
var trade = Commands.literal("trade")
|
||||||
.then(Commands.literal("debug")
|
.then(Commands.literal("debug")
|
||||||
.then(Commands.literal("init").executes(context -> initDebug(context.getSource())))
|
.requires(source -> TradeConfig.enableDebugFeaturesSafe())
|
||||||
|
.then(Commands.literal("init")
|
||||||
|
.executes(context -> initDebug(context.getSource(), 0))
|
||||||
|
.then(Commands.argument("time", IntegerArgumentType.integer(0))
|
||||||
|
.executes(context -> initDebug(context.getSource(), IntegerArgumentType.getInteger(context, "time")))))
|
||||||
|
.then(Commands.literal("request")
|
||||||
|
.executes(context -> requestDebug(context.getSource(), 0))
|
||||||
|
.then(Commands.argument("time", IntegerArgumentType.integer(0))
|
||||||
|
.executes(context -> requestDebug(context.getSource(), IntegerArgumentType.getInteger(context, "time")))))
|
||||||
.then(Commands.literal("offer")
|
.then(Commands.literal("offer")
|
||||||
.then(Commands.argument("spec", StringArgumentType.greedyString())
|
.then(Commands.argument("spec", StringArgumentType.greedyString())
|
||||||
.executes(context -> setDebugOffer(
|
.executes(context -> setDebugOffer(
|
||||||
@@ -28,12 +39,37 @@ public final class TradeCommand {
|
|||||||
.executes(context -> removeDebugOffer(
|
.executes(context -> removeDebugOffer(
|
||||||
context.getSource(),
|
context.getSource(),
|
||||||
StringArgumentType.getString(context, "spec")))))
|
StringArgumentType.getString(context, "spec")))))
|
||||||
|
.then(Commands.literal("unsafe")
|
||||||
|
.then(Commands.literal("damage").executes(context -> setDebugUnsafe(context.getSource(), DebugUnsafeState.DAMAGE, "damage")))
|
||||||
|
.then(Commands.literal("fire").executes(context -> setDebugUnsafe(context.getSource(), DebugUnsafeState.FIRE, "fire")))
|
||||||
|
.then(Commands.literal("liquid").executes(context -> setDebugUnsafe(context.getSource(), DebugUnsafeState.LIQUID, "liquid")))
|
||||||
|
.then(Commands.literal("moving").executes(context -> setDebugUnsafe(context.getSource(), DebugUnsafeState.MOVING, "moving")))
|
||||||
|
.then(Commands.literal("sleeping").executes(context -> setDebugUnsafe(context.getSource(), DebugUnsafeState.SLEEPING, "sleeping")))
|
||||||
|
.then(Commands.literal("riding").executes(context -> setDebugUnsafe(context.getSource(), DebugUnsafeState.RIDING, "riding")))
|
||||||
|
.then(Commands.literal("gliding").executes(context -> setDebugUnsafe(context.getSource(), DebugUnsafeState.GLIDING, "gliding")))
|
||||||
|
.then(Commands.literal("clear").executes(context -> clearDebugUnsafe(context.getSource()))))
|
||||||
.then(Commands.literal("accept").executes(context -> acceptDebug(context.getSource())))
|
.then(Commands.literal("accept").executes(context -> acceptDebug(context.getSource())))
|
||||||
.then(Commands.literal("cancel").executes(context -> cancelDebug(context.getSource())))
|
.then(Commands.literal("cancel").executes(context -> cancelDebug(context.getSource())))
|
||||||
.then(Commands.literal("close").executes(context -> closeDebug(context.getSource()))))
|
.then(Commands.literal("close").executes(context -> closeDebug(context.getSource()))));
|
||||||
|
|
||||||
|
dispatcher.register(trade
|
||||||
|
.then(Commands.literal("yes").executes(context -> respondTrade(context.getSource(), true)))
|
||||||
|
.then(Commands.literal("no").executes(context -> respondTrade(context.getSource(), false)))
|
||||||
|
.then(Commands.literal("toggle").executes(context -> toggleTrade(context.getSource())))
|
||||||
|
.then(Commands.literal("on").executes(context -> setTradeEnabled(context.getSource(), true)))
|
||||||
|
.then(Commands.literal("off").executes(context -> setTradeEnabled(context.getSource(), false)))
|
||||||
|
.then(Commands.literal("ignore")
|
||||||
|
.then(Commands.argument("player", StringArgumentType.word())
|
||||||
|
.suggests((context, builder) -> suggestPlayers(context.getSource(), builder))
|
||||||
|
.executes(context -> ignorePlayer(context.getSource(), StringArgumentType.getString(context, "player")))))
|
||||||
|
.then(Commands.literal("unignore")
|
||||||
|
.then(Commands.argument("player", StringArgumentType.word())
|
||||||
|
.suggests((context, builder) -> suggestPlayers(context.getSource(), builder))
|
||||||
|
.executes(context -> unignorePlayer(context.getSource(), StringArgumentType.getString(context, "player")))))
|
||||||
|
.then(Commands.literal("ignorelist").executes(context -> listIgnoredPlayers(context.getSource())))
|
||||||
.then(Commands.argument("player", StringArgumentType.word())
|
.then(Commands.argument("player", StringArgumentType.word())
|
||||||
.suggests((context, builder) -> suggestPlayers(context.getSource(), builder))
|
.suggests((context, builder) -> suggestPlayers(context.getSource(), builder))
|
||||||
.executes(context -> startTrade(context.getSource(), StringArgumentType.getString(context, "player")))));
|
.executes(context -> requestTrade(context.getSource(), StringArgumentType.getString(context, "player")))));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static CompletableFuture<com.mojang.brigadier.suggestion.Suggestions> suggestPlayers(CommandSourceStack source, SuggestionsBuilder builder) {
|
private static CompletableFuture<com.mojang.brigadier.suggestion.Suggestions> suggestPlayers(CommandSourceStack source, SuggestionsBuilder builder) {
|
||||||
@@ -45,7 +81,7 @@ public final class TradeCommand {
|
|||||||
return builder.buildFuture();
|
return builder.buildFuture();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static int startTrade(CommandSourceStack source, String targetName) {
|
private static int requestTrade(CommandSourceStack source, String targetName) {
|
||||||
if (!(source.getEntity() instanceof ServerPlayer player)) {
|
if (!(source.getEntity() instanceof ServerPlayer player)) {
|
||||||
source.sendFailure(Component.literal("Only players can start trades."));
|
source.sendFailure(Component.literal("Only players can start trades."));
|
||||||
return 0;
|
return 0;
|
||||||
@@ -62,27 +98,157 @@ public final class TradeCommand {
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!TradeManager.get(source.getServer()).startTrade(player, target)) {
|
if (!TradeManager.get(source.getServer()).requestTrade(player, target)) {
|
||||||
source.sendFailure(Component.literal("Trade could not be started. One of you is already trading."));
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
source.sendSuccess(() -> Component.literal("Trade opened with " + target.getGameProfile().getName() + "."), false);
|
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static int initDebug(CommandSourceStack source) {
|
private static int respondTrade(CommandSourceStack source, boolean accept) {
|
||||||
|
if (!(source.getEntity() instanceof ServerPlayer player)) {
|
||||||
|
source.sendFailure(Component.literal("Only players can answer trade requests."));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean result = accept
|
||||||
|
? acceptPendingTrade(player, source)
|
||||||
|
: TradeManager.get(source.getServer()).declinePendingTrade(player);
|
||||||
|
return result ? 1 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean acceptPendingTrade(ServerPlayer player, CommandSourceStack source) {
|
||||||
|
TradeManager manager = TradeManager.get(source.getServer());
|
||||||
|
if (manager.hasPendingTradeRequest(player)) {
|
||||||
|
return manager.acceptPendingTrade(player);
|
||||||
|
}
|
||||||
|
if (manager.hasPendingDebugTradeRequest(player)) {
|
||||||
|
return manager.acceptPendingDebugTrade(player);
|
||||||
|
}
|
||||||
|
player.sendSystemMessage(Component.literal("You have no pending trade request."));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int toggleTrade(CommandSourceStack source) {
|
||||||
|
if (!(source.getEntity() instanceof ServerPlayer player)) {
|
||||||
|
source.sendFailure(Component.literal("Only players can toggle trade requests."));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean enabled = TradeManager.get(source.getServer()).toggleTradeEnabled(player);
|
||||||
|
source.sendSuccess(() -> Component.literal("Trade requests are now " + (enabled ? "enabled" : "disabled") + "."), false);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int setTradeEnabled(CommandSourceStack source, boolean enabled) {
|
||||||
|
if (!(source.getEntity() instanceof ServerPlayer player)) {
|
||||||
|
source.sendFailure(Component.literal("Only players can change trade request settings."));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
TradeManager.get(source.getServer()).setTradeEnabled(player, enabled);
|
||||||
|
source.sendSuccess(() -> Component.literal("Trade requests are now " + (enabled ? "enabled" : "disabled") + "."), false);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int ignorePlayer(CommandSourceStack source, String targetName) {
|
||||||
|
if (!(source.getEntity() instanceof ServerPlayer player)) {
|
||||||
|
source.sendFailure(Component.literal("Only players can manage ignored traders."));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
ServerPlayer target = source.getServer().getPlayerList().getPlayerByName(targetName);
|
||||||
|
if (target == null) {
|
||||||
|
source.sendFailure(Component.literal("That player is not online."));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (target == player) {
|
||||||
|
source.sendFailure(Component.literal("You cannot ignore yourself."));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!TradeManager.get(source.getServer()).addIgnoredPlayer(player, target)) {
|
||||||
|
source.sendFailure(Component.literal(target.getGameProfile().getName() + " is already ignored."));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
source.sendSuccess(() -> Component.literal("You are now ignoring trade requests from " + target.getGameProfile().getName() + "."), false);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int unignorePlayer(CommandSourceStack source, String targetName) {
|
||||||
|
if (!(source.getEntity() instanceof ServerPlayer player)) {
|
||||||
|
source.sendFailure(Component.literal("Only players can manage ignored traders."));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
ServerPlayer target = source.getServer().getPlayerList().getPlayerByName(targetName);
|
||||||
|
if (target == null) {
|
||||||
|
source.sendFailure(Component.literal("That player is not online."));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!TradeManager.get(source.getServer()).removeIgnoredPlayer(player, target)) {
|
||||||
|
source.sendFailure(Component.literal(target.getGameProfile().getName() + " is not on your ignore list."));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
source.sendSuccess(() -> Component.literal("You are no longer ignoring trade requests from " + target.getGameProfile().getName() + "."), false);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int listIgnoredPlayers(CommandSourceStack source) {
|
||||||
|
if (!(source.getEntity() instanceof ServerPlayer player)) {
|
||||||
|
source.sendFailure(Component.literal("Only players can manage ignored traders."));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
java.util.List<String> ignored = TradeManager.get(source.getServer()).ignoredPlayerNames(player);
|
||||||
|
if (ignored.isEmpty()) {
|
||||||
|
source.sendSuccess(() -> Component.literal("Your trade ignore list is empty."), false);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
source.sendSuccess(() -> Component.literal("Ignored traders: " + String.join(", ", ignored)), false);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int initDebug(CommandSourceStack source, int delaySeconds) {
|
||||||
if (!(source.getEntity() instanceof ServerPlayer player)) {
|
if (!(source.getEntity() instanceof ServerPlayer player)) {
|
||||||
source.sendFailure(Component.literal("Only players can start debug trades."));
|
source.sendFailure(Component.literal("Only players can start debug trades."));
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!TradeManager.get(source.getServer()).initDebugTrade(player)) {
|
if (!TradeManager.get(source.getServer()).startDebugInit(player, delaySeconds)) {
|
||||||
source.sendFailure(Component.literal("You are already in a trade."));
|
source.sendFailure(Component.literal("You are already in a trade."));
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
source.sendSuccess(() -> Component.literal("Debug trade initialized."), false);
|
if (delaySeconds <= 0) {
|
||||||
|
source.sendSuccess(() -> Component.literal("Debug trade request sent and the debug trader will accept immediately."), false);
|
||||||
|
} else {
|
||||||
|
source.sendSuccess(() -> Component.literal("Debug trade request sent and the debug trader will accept in " + delaySeconds + " seconds."), false);
|
||||||
|
}
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int requestDebug(CommandSourceStack source, int delaySeconds) {
|
||||||
|
if (!(source.getEntity() instanceof ServerPlayer player)) {
|
||||||
|
source.sendFailure(Component.literal("Only players can start debug trades."));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!TradeManager.get(source.getServer()).scheduleDebugRequest(player, delaySeconds, false)) {
|
||||||
|
source.sendFailure(Component.literal("You are already in a trade."));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (delaySeconds <= 0) {
|
||||||
|
source.sendSuccess(() -> Component.literal("Debug trade request sent."), false);
|
||||||
|
} else {
|
||||||
|
source.sendSuccess(() -> Component.literal("Debug trade request scheduled in " + delaySeconds + " seconds."), false);
|
||||||
|
}
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -143,6 +309,36 @@ public final class TradeCommand {
|
|||||||
return runDebugAction(source, TradeManager.get(source.getServer())::closeDebug, "Debug trade closed.");
|
return runDebugAction(source, TradeManager.get(source.getServer())::closeDebug, "Debug trade closed.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static int setDebugUnsafe(CommandSourceStack source, DebugUnsafeState state, String label) {
|
||||||
|
if (!(source.getEntity() instanceof ServerPlayer player)) {
|
||||||
|
source.sendFailure(Component.literal("Only players can control debug trades."));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!TradeManager.get(source.getServer()).setDebugUnsafeState(player, state)) {
|
||||||
|
source.sendFailure(Component.literal("Start a debug trade first with /trade debug init."));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
source.sendSuccess(() -> Component.literal("Debug unsafe state enabled: " + label + "."), false);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int clearDebugUnsafe(CommandSourceStack source) {
|
||||||
|
if (!(source.getEntity() instanceof ServerPlayer player)) {
|
||||||
|
source.sendFailure(Component.literal("Only players can control debug trades."));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!TradeManager.get(source.getServer()).clearDebugUnsafeStates(player)) {
|
||||||
|
source.sendFailure(Component.literal("Start a debug trade first with /trade debug init."));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
source.sendSuccess(() -> Component.literal("Debug unsafe states cleared."), false);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
private static int runDebugAction(CommandSourceStack source, java.util.function.Predicate<ServerPlayer> action, String successMessage) {
|
private static int runDebugAction(CommandSourceStack source, java.util.function.Predicate<ServerPlayer> action, String successMessage) {
|
||||||
if (!(source.getEntity() instanceof ServerPlayer player)) {
|
if (!(source.getEntity() instanceof ServerPlayer player)) {
|
||||||
source.sendFailure(Component.literal("Only players can control debug trades."));
|
source.sendFailure(Component.literal("Only players can control debug trades."));
|
||||||
|
|||||||
31
src/main/java/com/trunksbomb/trade/mod/TradeAuditLog.java
Normal file
31
src/main/java/com/trunksbomb/trade/mod/TradeAuditLog.java
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
package com.trunksbomb.trade.mod;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.StandardOpenOption;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import net.minecraft.server.MinecraftServer;
|
||||||
|
import net.neoforged.fml.loading.FMLPaths;
|
||||||
|
|
||||||
|
public final class TradeAuditLog {
|
||||||
|
private static final DateTimeFormatter TIMESTAMP_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
||||||
|
private static final Object LOCK = new Object();
|
||||||
|
|
||||||
|
private TradeAuditLog() {}
|
||||||
|
|
||||||
|
public static void log(MinecraftServer server, String message) {
|
||||||
|
Path logPath = FMLPaths.GAMEDIR.get().resolve("logs").resolve("trade.log");
|
||||||
|
String line = "[" + LocalDateTime.now().format(TIMESTAMP_FORMAT) + "] " + message + System.lineSeparator();
|
||||||
|
|
||||||
|
synchronized (LOCK) {
|
||||||
|
try {
|
||||||
|
Files.createDirectories(logPath.getParent());
|
||||||
|
Files.writeString(logPath, line, StandardOpenOption.CREATE, StandardOpenOption.APPEND);
|
||||||
|
} catch (IOException exception) {
|
||||||
|
TradeMod.LOGGER.warn("Failed to write trade audit log entry", exception);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,18 +1,52 @@
|
|||||||
package com.trunksbomb.trade.mod;
|
package com.trunksbomb.trade.mod;
|
||||||
|
|
||||||
import com.trunksbomb.trade.mod.client.TradeClientState;
|
import com.mojang.blaze3d.platform.InputConstants;
|
||||||
|
import com.trunksbomb.trade.client.TradeClientState;
|
||||||
|
import com.trunksbomb.trade.network.TradeRequestPayload;
|
||||||
|
import net.minecraft.client.KeyMapping;
|
||||||
import net.minecraft.client.Minecraft;
|
import net.minecraft.client.Minecraft;
|
||||||
|
import net.minecraft.network.chat.Component;
|
||||||
|
import net.minecraft.world.entity.player.Player;
|
||||||
import net.neoforged.api.distmarker.Dist;
|
import net.neoforged.api.distmarker.Dist;
|
||||||
import net.neoforged.bus.api.SubscribeEvent;
|
import net.neoforged.bus.api.IEventBus;
|
||||||
import net.neoforged.fml.common.EventBusSubscriber;
|
|
||||||
import net.neoforged.fml.common.Mod;
|
import net.neoforged.fml.common.Mod;
|
||||||
import net.neoforged.fml.event.lifecycle.FMLClientSetupEvent;
|
import net.neoforged.neoforge.client.event.ClientTickEvent;
|
||||||
|
import net.neoforged.neoforge.client.event.RegisterKeyMappingsEvent;
|
||||||
|
import net.neoforged.neoforge.common.NeoForge;
|
||||||
|
import net.neoforged.neoforge.network.PacketDistributor;
|
||||||
|
import org.lwjgl.glfw.GLFW;
|
||||||
|
|
||||||
@Mod(value = TradeMod.MODID, dist = Dist.CLIENT)
|
@Mod(value = TradeMod.MODID, dist = Dist.CLIENT)
|
||||||
@EventBusSubscriber(modid = TradeMod.MODID, value = Dist.CLIENT)
|
|
||||||
public class TradeClientMod {
|
public class TradeClientMod {
|
||||||
@SubscribeEvent
|
private static final KeyMapping REQUEST_TRADE = new KeyMapping(
|
||||||
static void onClientSetup(FMLClientSetupEvent event) {
|
"key.trade.request_trade",
|
||||||
event.enqueueWork(() -> TradeClientState.init(Minecraft.getInstance()));
|
InputConstants.Type.KEYSYM,
|
||||||
|
GLFW.GLFW_KEY_G,
|
||||||
|
"key.categories.gameplay");
|
||||||
|
|
||||||
|
public TradeClientMod(IEventBus modEventBus) {
|
||||||
|
modEventBus.addListener(this::registerKeyMappings);
|
||||||
|
NeoForge.EVENT_BUS.addListener(this::onClientTick);
|
||||||
|
TradeClientState.init(Minecraft.getInstance());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void registerKeyMappings(RegisterKeyMappingsEvent event) {
|
||||||
|
event.register(REQUEST_TRADE);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onClientTick(ClientTickEvent.Post event) {
|
||||||
|
Minecraft minecraft = Minecraft.getInstance();
|
||||||
|
while (REQUEST_TRADE.consumeClick()) {
|
||||||
|
if (minecraft.player == null || minecraft.screen != null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(minecraft.crosshairPickEntity instanceof Player target) || target == minecraft.player) {
|
||||||
|
minecraft.player.displayClientMessage(Component.literal("Look at a player to send a trade request."), true);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
PacketDistributor.sendToServer(new TradeRequestPayload(target.getId()));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
124
src/main/java/com/trunksbomb/trade/mod/TradeConfig.java
Normal file
124
src/main/java/com/trunksbomb/trade/mod/TradeConfig.java
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
package com.trunksbomb.trade.mod;
|
||||||
|
|
||||||
|
import net.neoforged.neoforge.common.ModConfigSpec;
|
||||||
|
|
||||||
|
public final class TradeConfig {
|
||||||
|
public static final ModConfigSpec SPEC;
|
||||||
|
private static final ModConfigSpec.IntValue TRADE_COMMAND_PROXIMITY;
|
||||||
|
private static final ModConfigSpec.IntValue REQUEST_TIMEOUT_SECONDS;
|
||||||
|
private static final ModConfigSpec.BooleanValue ENABLE_DEBUG_FEATURES;
|
||||||
|
private static final ModConfigSpec.BooleanValue REQUIRE_ON_GROUND;
|
||||||
|
private static final ModConfigSpec.BooleanValue REQUIRE_STATIONARY;
|
||||||
|
private static final ModConfigSpec.DoubleValue STATIONARY_SPEED_THRESHOLD;
|
||||||
|
private static final ModConfigSpec.BooleanValue REQUIRE_NO_RECENT_DAMAGE;
|
||||||
|
private static final ModConfigSpec.IntValue NO_DAMAGE_SECONDS;
|
||||||
|
private static final ModConfigSpec.BooleanValue REQUIRE_NOT_ON_FIRE;
|
||||||
|
private static final ModConfigSpec.BooleanValue REQUIRE_NOT_IN_LIQUID;
|
||||||
|
private static final ModConfigSpec.BooleanValue REQUIRE_NOT_SLEEPING;
|
||||||
|
private static final ModConfigSpec.BooleanValue REQUIRE_NOT_FALL_FLYING;
|
||||||
|
private static final ModConfigSpec.BooleanValue REQUIRE_NOT_RIDING;
|
||||||
|
private static final ModConfigSpec.BooleanValue REQUIRE_SAME_DIMENSION;
|
||||||
|
|
||||||
|
static {
|
||||||
|
ModConfigSpec.Builder builder = new ModConfigSpec.Builder();
|
||||||
|
builder.push("trade");
|
||||||
|
TRADE_COMMAND_PROXIMITY = builder
|
||||||
|
.comment("Maximum distance in blocks to initiate or accept a trade request. 0 disables the distance check.")
|
||||||
|
.defineInRange("tradeCommandProximity", 0, 0, 1024);
|
||||||
|
REQUEST_TIMEOUT_SECONDS = builder
|
||||||
|
.comment("Seconds before a trade request expires.")
|
||||||
|
.defineInRange("requestTimeoutSeconds", 30, 1, 3600);
|
||||||
|
ENABLE_DEBUG_FEATURES = builder.comment("Enable debug trade commands and debug UI/testing tools.")
|
||||||
|
.define("enableDebugFeatures", false);
|
||||||
|
REQUIRE_ON_GROUND = builder.comment("Require players to be on solid ground before requesting or accepting a trade.")
|
||||||
|
.define("requireOnGround", true);
|
||||||
|
REQUIRE_STATIONARY = builder.comment("Require players to be stationary before requesting or accepting a trade.")
|
||||||
|
.define("requireStationary", true);
|
||||||
|
STATIONARY_SPEED_THRESHOLD = builder.comment("Maximum horizontal player speed considered stationary.")
|
||||||
|
.defineInRange("stationarySpeedThreshold", 0.03D, 0.0D, 10.0D);
|
||||||
|
REQUIRE_NO_RECENT_DAMAGE = builder.comment("Require players to have taken no damage recently before requesting or accepting a trade.")
|
||||||
|
.define("requireNoRecentDamage", true);
|
||||||
|
NO_DAMAGE_SECONDS = builder.comment("How long a player must be out of combat to trade when requireNoRecentDamage is enabled.")
|
||||||
|
.defineInRange("noDamageSeconds", 10, 0, 3600);
|
||||||
|
REQUIRE_NOT_ON_FIRE = builder.comment("Require players to not be on fire before requesting or accepting a trade.")
|
||||||
|
.define("requireNotOnFire", true);
|
||||||
|
REQUIRE_NOT_IN_LIQUID = builder.comment("Require players to not be in water or lava before requesting or accepting a trade.")
|
||||||
|
.define("requireNotInLiquid", true);
|
||||||
|
REQUIRE_NOT_SLEEPING = builder.comment("Require players to not be sleeping before requesting or accepting a trade.")
|
||||||
|
.define("requireNotSleeping", true);
|
||||||
|
REQUIRE_NOT_FALL_FLYING = builder.comment("Require players to not be gliding with elytra before requesting or accepting a trade.")
|
||||||
|
.define("requireNotFallFlying", true);
|
||||||
|
REQUIRE_NOT_RIDING = builder.comment("Require players to not be riding another entity before requesting or accepting a trade.")
|
||||||
|
.define("requireNotRiding", true);
|
||||||
|
REQUIRE_SAME_DIMENSION = builder.comment("Require both players to be in the same dimension.")
|
||||||
|
.define("requireSameDimension", true);
|
||||||
|
builder.pop();
|
||||||
|
SPEC = builder.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private TradeConfig() {}
|
||||||
|
|
||||||
|
public static int tradeCommandProximity() {
|
||||||
|
return TRADE_COMMAND_PROXIMITY.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int requestTimeoutSeconds() {
|
||||||
|
return REQUEST_TIMEOUT_SECONDS.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean enableDebugFeatures() {
|
||||||
|
return ENABLE_DEBUG_FEATURES.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean enableDebugFeaturesSafe() {
|
||||||
|
try {
|
||||||
|
return ENABLE_DEBUG_FEATURES.get();
|
||||||
|
} catch (IllegalStateException ignored) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean requireOnGround() {
|
||||||
|
return REQUIRE_ON_GROUND.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean requireStationary() {
|
||||||
|
return REQUIRE_STATIONARY.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static double stationarySpeedThreshold() {
|
||||||
|
return STATIONARY_SPEED_THRESHOLD.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean requireNoRecentDamage() {
|
||||||
|
return REQUIRE_NO_RECENT_DAMAGE.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int noDamageSeconds() {
|
||||||
|
return NO_DAMAGE_SECONDS.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean requireNotOnFire() {
|
||||||
|
return REQUIRE_NOT_ON_FIRE.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean requireNotInLiquid() {
|
||||||
|
return REQUIRE_NOT_IN_LIQUID.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean requireNotSleeping() {
|
||||||
|
return REQUIRE_NOT_SLEEPING.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean requireNotFallFlying() {
|
||||||
|
return REQUIRE_NOT_FALL_FLYING.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean requireNotRiding() {
|
||||||
|
return REQUIRE_NOT_RIDING.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean requireSameDimension() {
|
||||||
|
return REQUIRE_SAME_DIMENSION.get();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,10 +1,11 @@
|
|||||||
package com.trunksbomb.trade.mod;
|
package com.trunksbomb.trade.mod;
|
||||||
|
|
||||||
import com.mojang.logging.LogUtils;
|
import com.mojang.logging.LogUtils;
|
||||||
import com.trunksbomb.trade.mod.command.TradeCommand;
|
import com.trunksbomb.trade.command.TradeCommand;
|
||||||
import com.trunksbomb.trade.mod.network.TradeNetworking;
|
import com.trunksbomb.trade.network.TradeNetworking;
|
||||||
import com.trunksbomb.trade.mod.trade.TradeManager;
|
import com.trunksbomb.trade.trade.TradeManager;
|
||||||
import net.neoforged.bus.api.IEventBus;
|
import net.neoforged.bus.api.IEventBus;
|
||||||
|
import net.neoforged.fml.ModContainer;
|
||||||
import net.neoforged.fml.common.Mod;
|
import net.neoforged.fml.common.Mod;
|
||||||
import net.neoforged.fml.event.lifecycle.FMLCommonSetupEvent;
|
import net.neoforged.fml.event.lifecycle.FMLCommonSetupEvent;
|
||||||
import net.neoforged.neoforge.common.NeoForge;
|
import net.neoforged.neoforge.common.NeoForge;
|
||||||
@@ -18,11 +19,15 @@ public class TradeMod {
|
|||||||
public static final String MODID = "trade";
|
public static final String MODID = "trade";
|
||||||
public static final Logger LOGGER = LogUtils.getLogger();
|
public static final Logger LOGGER = LogUtils.getLogger();
|
||||||
|
|
||||||
public TradeMod(IEventBus modEventBus) {
|
public TradeMod(IEventBus modEventBus, ModContainer modContainer) {
|
||||||
modEventBus.addListener(this::commonSetup);
|
modEventBus.addListener(this::commonSetup);
|
||||||
modEventBus.addListener(this::registerPayloads);
|
modEventBus.addListener(this::registerPayloads);
|
||||||
|
modContainer.registerConfig(net.neoforged.fml.config.ModConfig.Type.SERVER, TradeConfig.SPEC);
|
||||||
NeoForge.EVENT_BUS.addListener(this::registerCommands);
|
NeoForge.EVENT_BUS.addListener(this::registerCommands);
|
||||||
NeoForge.EVENT_BUS.addListener(this::onPlayerLogout);
|
NeoForge.EVENT_BUS.addListener(this::onPlayerLogout);
|
||||||
|
NeoForge.EVENT_BUS.addListener(this::onItemPickup);
|
||||||
|
NeoForge.EVENT_BUS.addListener(this::onServerTick);
|
||||||
|
NeoForge.EVENT_BUS.addListener(this::onLivingDamage);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void commonSetup(FMLCommonSetupEvent event) {
|
private void commonSetup(FMLCommonSetupEvent event) {
|
||||||
@@ -43,4 +48,21 @@ public class TradeMod {
|
|||||||
TradeManager.get(player.server).handleDisconnect(player);
|
TradeManager.get(player.server).handleDisconnect(player);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void onItemPickup(net.neoforged.neoforge.event.entity.player.ItemEntityPickupEvent.Pre event) {
|
||||||
|
if (event.getPlayer() instanceof net.minecraft.server.level.ServerPlayer player
|
||||||
|
&& TradeManager.get(player.server).isTrading(player)) {
|
||||||
|
event.setCanPickup(net.neoforged.neoforge.common.util.TriState.FALSE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void onServerTick(net.neoforged.neoforge.event.tick.ServerTickEvent.Post event) {
|
||||||
|
TradeManager.get(event.getServer()).tick(event.getServer());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void onLivingDamage(net.neoforged.neoforge.event.entity.living.LivingDamageEvent.Post event) {
|
||||||
|
if (event.getEntity() instanceof net.minecraft.server.level.ServerPlayer player) {
|
||||||
|
TradeManager.get(player.server).recordDamage(player);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
package com.trunksbomb.trade.mod.network;
|
package com.trunksbomb.trade.network;
|
||||||
|
|
||||||
import com.trunksbomb.trade.mod.TradeMod;
|
import com.trunksbomb.trade.mod.TradeMod;
|
||||||
import com.trunksbomb.trade.mod.trade.DebugControlAction;
|
import com.trunksbomb.trade.trade.DebugControlAction;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import net.minecraft.network.RegistryFriendlyByteBuf;
|
import net.minecraft.network.RegistryFriendlyByteBuf;
|
||||||
import net.minecraft.network.codec.StreamCodec;
|
import net.minecraft.network.codec.StreamCodec;
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
package com.trunksbomb.trade.mod.network;
|
package com.trunksbomb.trade.network;
|
||||||
|
|
||||||
import com.trunksbomb.trade.mod.TradeMod;
|
import com.trunksbomb.trade.mod.TradeMod;
|
||||||
import com.trunksbomb.trade.mod.trade.TradeAction;
|
import com.trunksbomb.trade.trade.TradeAction;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import net.minecraft.network.RegistryFriendlyByteBuf;
|
import net.minecraft.network.RegistryFriendlyByteBuf;
|
||||||
import net.minecraft.network.codec.StreamCodec;
|
import net.minecraft.network.codec.StreamCodec;
|
||||||
import net.minecraft.network.protocol.common.custom.CustomPacketPayload;
|
import net.minecraft.network.protocol.common.custom.CustomPacketPayload;
|
||||||
import net.minecraft.resources.ResourceLocation;
|
import net.minecraft.resources.ResourceLocation;
|
||||||
|
|
||||||
public record TradeActionPayload(UUID sessionId, TradeAction action, int slot, boolean secondary) implements CustomPacketPayload {
|
public record TradeActionPayload(UUID sessionId, TradeAction action, int slot, int amount) implements CustomPacketPayload {
|
||||||
public static final Type<TradeActionPayload> TYPE = new Type<>(ResourceLocation.fromNamespaceAndPath(TradeMod.MODID, "trade_action"));
|
public static final Type<TradeActionPayload> TYPE = new Type<>(ResourceLocation.fromNamespaceAndPath(TradeMod.MODID, "trade_action"));
|
||||||
public static final StreamCodec<RegistryFriendlyByteBuf, TradeActionPayload> STREAM_CODEC = new StreamCodec<>() {
|
public static final StreamCodec<RegistryFriendlyByteBuf, TradeActionPayload> STREAM_CODEC = new StreamCodec<>() {
|
||||||
@Override
|
@Override
|
||||||
@@ -16,12 +16,12 @@ public record TradeActionPayload(UUID sessionId, TradeAction action, int slot, b
|
|||||||
buf.writeUUID(value.sessionId);
|
buf.writeUUID(value.sessionId);
|
||||||
buf.writeEnum(value.action);
|
buf.writeEnum(value.action);
|
||||||
buf.writeVarInt(value.slot);
|
buf.writeVarInt(value.slot);
|
||||||
buf.writeBoolean(value.secondary);
|
buf.writeVarInt(value.amount);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public TradeActionPayload decode(RegistryFriendlyByteBuf buf) {
|
public TradeActionPayload decode(RegistryFriendlyByteBuf buf) {
|
||||||
return new TradeActionPayload(buf.readUUID(), buf.readEnum(TradeAction.class), buf.readVarInt(), buf.readBoolean());
|
return new TradeActionPayload(buf.readUUID(), buf.readEnum(TradeAction.class), buf.readVarInt(), buf.readVarInt());
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.trunksbomb.trade.mod.network;
|
package com.trunksbomb.trade.network;
|
||||||
|
|
||||||
import com.trunksbomb.trade.mod.TradeMod;
|
import com.trunksbomb.trade.mod.TradeMod;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
package com.trunksbomb.trade.mod.network;
|
package com.trunksbomb.trade.network;
|
||||||
|
|
||||||
import com.trunksbomb.trade.mod.client.TradeScreen;
|
import com.trunksbomb.trade.trade.TradeManager;
|
||||||
import com.trunksbomb.trade.mod.trade.TradeManager;
|
|
||||||
import net.minecraft.client.Minecraft;
|
|
||||||
import net.minecraft.network.chat.Component;
|
import net.minecraft.network.chat.Component;
|
||||||
import net.minecraft.server.level.ServerPlayer;
|
import net.minecraft.server.level.ServerPlayer;
|
||||||
import net.neoforged.neoforge.network.handling.DirectionalPayloadHandler;
|
import net.neoforged.neoforge.network.handling.DirectionalPayloadHandler;
|
||||||
@@ -25,6 +23,10 @@ public final class TradeNetworking {
|
|||||||
TradeActionPayload.TYPE,
|
TradeActionPayload.TYPE,
|
||||||
TradeActionPayload.STREAM_CODEC,
|
TradeActionPayload.STREAM_CODEC,
|
||||||
TradeNetworking::handleTradeActionServer);
|
TradeNetworking::handleTradeActionServer);
|
||||||
|
registrar.playToServer(
|
||||||
|
TradeRequestPayload.TYPE,
|
||||||
|
TradeRequestPayload.STREAM_CODEC,
|
||||||
|
TradeNetworking::handleTradeRequestServer);
|
||||||
registrar.playToServer(
|
registrar.playToServer(
|
||||||
DebugTradeControlPayload.TYPE,
|
DebugTradeControlPayload.TYPE,
|
||||||
DebugTradeControlPayload.STREAM_CODEC,
|
DebugTradeControlPayload.STREAM_CODEC,
|
||||||
@@ -32,39 +34,32 @@ public final class TradeNetworking {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static void handleTradeStateClient(TradeStatePayload payload, IPayloadContext context) {
|
private static void handleTradeStateClient(TradeStatePayload payload, IPayloadContext context) {
|
||||||
Minecraft minecraft = Minecraft.getInstance();
|
invokeClientState("showOrUpdate", new Class<?>[] {payload.view().getClass()}, payload.view());
|
||||||
if (minecraft.player == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (minecraft.screen instanceof TradeScreen screen && screen.sessionId().equals(payload.view().sessionId())) {
|
|
||||||
screen.updateView(payload.view());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
minecraft.setScreen(new TradeScreen(
|
|
||||||
new TradeScreen.TradeMenu(0, minecraft.player.getInventory(), payload.view()),
|
|
||||||
minecraft.player.getInventory(),
|
|
||||||
Component.literal("Trade")));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void handleTradeCloseClient(TradeClosePayload payload, IPayloadContext context) {
|
private static void handleTradeCloseClient(TradeClosePayload payload, IPayloadContext context) {
|
||||||
Minecraft minecraft = Minecraft.getInstance();
|
invokeClientState("closeTrade", new Class<?>[] {java.util.UUID.class, Component.class}, payload.sessionId(), payload.reason());
|
||||||
if (minecraft.screen instanceof TradeScreen screen && screen.sessionId().equals(payload.sessionId())) {
|
|
||||||
minecraft.setScreen(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (minecraft.player != null) {
|
|
||||||
minecraft.player.displayClientMessage(payload.reason(), false);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void handleTradeActionServer(TradeActionPayload payload, IPayloadContext context) {
|
private static void handleTradeActionServer(TradeActionPayload payload, IPayloadContext context) {
|
||||||
if (context.player() instanceof ServerPlayer player) {
|
if (context.player() instanceof ServerPlayer player) {
|
||||||
TradeManager.get(player.server).handleAction(player, payload.sessionId(), payload.action(), payload.slot(), payload.secondary());
|
TradeManager.get(player.server).handleAction(player, payload.sessionId(), payload.action(), payload.slot(), payload.amount());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void handleTradeRequestServer(TradeRequestPayload payload, IPayloadContext context) {
|
||||||
|
if (!(context.player() instanceof ServerPlayer player)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(player.serverLevel().getEntity(payload.targetEntityId()) instanceof ServerPlayer target)) {
|
||||||
|
player.sendSystemMessage(Component.literal("You must be looking at a player to trade."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
TradeManager.get(player.server).requestTrade(player, target);
|
||||||
|
}
|
||||||
|
|
||||||
private static void handleDebugTradeControlServer(DebugTradeControlPayload payload, IPayloadContext context) {
|
private static void handleDebugTradeControlServer(DebugTradeControlPayload payload, IPayloadContext context) {
|
||||||
if (context.player() instanceof ServerPlayer player) {
|
if (context.player() instanceof ServerPlayer player) {
|
||||||
TradeManager.get(player.server).handleDebugControl(player, payload.sessionId(), payload.action(), payload.spec());
|
TradeManager.get(player.server).handleDebugControl(player, payload.sessionId(), payload.action(), payload.spec());
|
||||||
@@ -74,4 +69,13 @@ public final class TradeNetworking {
|
|||||||
private static void ignoreTradeStateServer(TradeStatePayload payload, IPayloadContext context) {}
|
private static void ignoreTradeStateServer(TradeStatePayload payload, IPayloadContext context) {}
|
||||||
|
|
||||||
private static void ignoreTradeCloseServer(TradeClosePayload payload, IPayloadContext context) {}
|
private static void ignoreTradeCloseServer(TradeClosePayload payload, IPayloadContext context) {}
|
||||||
|
|
||||||
|
private static void invokeClientState(String methodName, Class<?>[] parameterTypes, Object... arguments) {
|
||||||
|
try {
|
||||||
|
Class<?> clientState = Class.forName("com.trunksbomb.trade.client.TradeClientState");
|
||||||
|
clientState.getMethod(methodName, parameterTypes).invoke(null, arguments);
|
||||||
|
} catch (ReflectiveOperationException exception) {
|
||||||
|
throw new RuntimeException("Failed to invoke TradeClientState#" + methodName, exception);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package com.trunksbomb.trade.network;
|
||||||
|
|
||||||
|
import com.trunksbomb.trade.mod.TradeMod;
|
||||||
|
import net.minecraft.network.RegistryFriendlyByteBuf;
|
||||||
|
import net.minecraft.network.codec.StreamCodec;
|
||||||
|
import net.minecraft.network.protocol.common.custom.CustomPacketPayload;
|
||||||
|
import net.minecraft.resources.ResourceLocation;
|
||||||
|
|
||||||
|
public record TradeRequestPayload(int targetEntityId) implements CustomPacketPayload {
|
||||||
|
public static final Type<TradeRequestPayload> TYPE = new Type<>(ResourceLocation.fromNamespaceAndPath(TradeMod.MODID, "trade_request"));
|
||||||
|
public static final StreamCodec<RegistryFriendlyByteBuf, TradeRequestPayload> STREAM_CODEC = new StreamCodec<>() {
|
||||||
|
@Override
|
||||||
|
public void encode(RegistryFriendlyByteBuf buf, TradeRequestPayload value) {
|
||||||
|
buf.writeVarInt(value.targetEntityId());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public TradeRequestPayload decode(RegistryFriendlyByteBuf buf) {
|
||||||
|
return new TradeRequestPayload(buf.readVarInt());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Type<? extends CustomPacketPayload> type() {
|
||||||
|
return TYPE;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
package com.trunksbomb.trade.mod.network;
|
package com.trunksbomb.trade.network;
|
||||||
|
|
||||||
import com.trunksbomb.trade.mod.TradeMod;
|
import com.trunksbomb.trade.mod.TradeMod;
|
||||||
import com.trunksbomb.trade.mod.trade.TradeView;
|
import com.trunksbomb.trade.trade.TradeView;
|
||||||
import net.minecraft.network.RegistryFriendlyByteBuf;
|
import net.minecraft.network.RegistryFriendlyByteBuf;
|
||||||
import net.minecraft.network.codec.StreamCodec;
|
import net.minecraft.network.codec.StreamCodec;
|
||||||
import net.minecraft.network.protocol.common.custom.CustomPacketPayload;
|
import net.minecraft.network.protocol.common.custom.CustomPacketPayload;
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
package com.trunksbomb.trade.mod.trade;
|
package com.trunksbomb.trade.trade;
|
||||||
|
|
||||||
public enum DebugControlAction {
|
public enum DebugControlAction {
|
||||||
SET_OFFER,
|
SET_OFFER,
|
||||||
APPEND_RANDOM,
|
APPEND_RANDOM,
|
||||||
REMOVE_OFFER,
|
REMOVE_OFFER,
|
||||||
REMOVE_LAST,
|
REMOVE_LAST,
|
||||||
|
SET_UNSAFE,
|
||||||
|
CLEAR_UNSAFE,
|
||||||
ACCEPT,
|
ACCEPT,
|
||||||
CANCEL,
|
CANCEL,
|
||||||
CLOSE
|
CLOSE
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
package com.trunksbomb.trade.mod.trade;
|
package com.trunksbomb.trade.trade;
|
||||||
|
|
||||||
import com.trunksbomb.trade.mod.network.TradeClosePayload;
|
import com.trunksbomb.trade.network.TradeClosePayload;
|
||||||
import com.trunksbomb.trade.mod.network.TradeStatePayload;
|
import com.trunksbomb.trade.network.TradeStatePayload;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.EnumSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import net.minecraft.core.registries.BuiltInRegistries;
|
import net.minecraft.core.registries.BuiltInRegistries;
|
||||||
@@ -22,14 +23,17 @@ public class DebugTradeSession {
|
|||||||
|
|
||||||
private final UUID id = UUID.randomUUID();
|
private final UUID id = UUID.randomUUID();
|
||||||
private final ServerPlayer player;
|
private final ServerPlayer player;
|
||||||
private final List<ItemStack> selfOffer = blankOffer();
|
private final List<ItemStack> inventorySnapshot;
|
||||||
|
private final List<TradeEntry> selfOffer = blankOffer();
|
||||||
private final List<ItemStack> otherOffer = blankStacks();
|
private final List<ItemStack> otherOffer = blankStacks();
|
||||||
|
private final EnumSet<DebugUnsafeState> unsafeStates = EnumSet.noneOf(DebugUnsafeState.class);
|
||||||
private boolean selfAccepted;
|
private boolean selfAccepted;
|
||||||
private boolean otherAccepted;
|
private boolean otherAccepted;
|
||||||
private TradeStage stage = TradeStage.OFFERING;
|
private TradeStage stage = TradeStage.OFFERING;
|
||||||
|
|
||||||
public DebugTradeSession(ServerPlayer player) {
|
public DebugTradeSession(ServerPlayer player) {
|
||||||
this.player = player;
|
this.player = player;
|
||||||
|
this.inventorySnapshot = inventorySnapshot(player);
|
||||||
}
|
}
|
||||||
|
|
||||||
public UUID id() {
|
public UUID id() {
|
||||||
@@ -40,6 +44,22 @@ public class DebugTradeSession {
|
|||||||
return player;
|
return player;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public List<ItemStack> selfOfferSnapshot() {
|
||||||
|
List<ItemStack> result = new ArrayList<>(OFFER_SLOT_COUNT);
|
||||||
|
for (TradeEntry entry : selfOffer) {
|
||||||
|
result.add(entry == null ? ItemStack.EMPTY : entry.stack().copy());
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<ItemStack> otherOfferSnapshot() {
|
||||||
|
List<ItemStack> result = new ArrayList<>(OFFER_SLOT_COUNT);
|
||||||
|
for (ItemStack stack : otherOffer) {
|
||||||
|
result.add(stack.copy());
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
public void sync() {
|
public void sync() {
|
||||||
PacketDistributor.sendToPlayer(player, new TradeStatePayload(view()));
|
PacketDistributor.sendToPlayer(player, new TradeStatePayload(view()));
|
||||||
}
|
}
|
||||||
@@ -48,59 +68,60 @@ public class DebugTradeSession {
|
|||||||
PacketDistributor.sendToPlayer(player, new TradeClosePayload(id, reason));
|
PacketDistributor.sendToPlayer(player, new TradeClosePayload(id, reason));
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean addFromInventory(int inventorySlot, boolean singleItem) {
|
public boolean addFromInventory(int inventorySlot, int amount) {
|
||||||
if (stage != TradeStage.OFFERING || inventorySlot < 0 || inventorySlot >= INVENTORY_SLOT_COUNT) {
|
if (stage != TradeStage.OFFERING || inventorySlot < 0 || inventorySlot >= INVENTORY_SLOT_COUNT) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
Inventory inventory = player.getInventory();
|
ItemStack sourceStack = inventorySnapshot.get(inventorySlot);
|
||||||
ItemStack sourceStack = inventory.getItem(inventorySlot);
|
|
||||||
if (sourceStack.isEmpty()) {
|
if (sourceStack.isEmpty()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
ItemStack moving = sourceStack.copyWithCount(singleItem ? 1 : sourceStack.getCount());
|
int available = sourceStack.getCount() - reservedCount(inventorySlot);
|
||||||
if (!insertStack(selfOffer, moving)) {
|
if (available <= 0) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
sourceStack.shrink(singleItem ? 1 : sourceStack.getCount());
|
int moveAmount = Math.max(1, Math.min(amount, available));
|
||||||
if (sourceStack.isEmpty()) {
|
for (int i = 0; i < selfOffer.size(); i++) {
|
||||||
inventory.setItem(inventorySlot, ItemStack.EMPTY);
|
TradeEntry entry = selfOffer.get(i);
|
||||||
|
if (entry != null && entry.sourceSlot() == inventorySlot && ItemStack.isSameItemSameComponents(entry.stack(), sourceStack)) {
|
||||||
|
ItemStack merged = entry.stack().copy();
|
||||||
|
merged.grow(moveAmount);
|
||||||
|
selfOffer.set(i, new TradeEntry(inventorySlot, merged));
|
||||||
|
clearAccepts();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
inventory.setChanged();
|
|
||||||
|
int freeSlot = firstFreeOfferSlot(selfOffer);
|
||||||
|
if (freeSlot == -1) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
selfOffer.set(freeSlot, new TradeEntry(inventorySlot, sourceStack.copyWithCount(moveAmount)));
|
||||||
clearAccepts();
|
clearAccepts();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean removeFromOffer(int offerSlot, boolean singleItem) {
|
public boolean removeFromOffer(int offerSlot, int amount) {
|
||||||
if (stage != TradeStage.OFFERING || offerSlot < 0 || offerSlot >= OFFER_SLOT_COUNT) {
|
if (stage != TradeStage.OFFERING || offerSlot < 0 || offerSlot >= OFFER_SLOT_COUNT) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
ItemStack offered = selfOffer.get(offerSlot);
|
TradeEntry entry = selfOffer.get(offerSlot);
|
||||||
if (offered.isEmpty()) {
|
if (entry == null) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
List<ItemStack> workingInventory = inventorySnapshot();
|
int moveAmount = Math.max(1, Math.min(amount, entry.stack().getCount()));
|
||||||
ItemStack returned = offered.copyWithCount(singleItem ? 1 : offered.getCount());
|
if (moveAmount < entry.stack().getCount()) {
|
||||||
if (!insertStack(workingInventory, returned)) {
|
ItemStack updated = entry.stack().copy();
|
||||||
return false;
|
updated.shrink(moveAmount);
|
||||||
}
|
selfOffer.set(offerSlot, new TradeEntry(entry.sourceSlot(), updated));
|
||||||
|
|
||||||
Inventory inventory = player.getInventory();
|
|
||||||
for (int i = 0; i < workingInventory.size(); i++) {
|
|
||||||
inventory.setItem(i, workingInventory.get(i));
|
|
||||||
}
|
|
||||||
inventory.setChanged();
|
|
||||||
|
|
||||||
if (singleItem && offered.getCount() > 1) {
|
|
||||||
ItemStack updated = offered.copy();
|
|
||||||
updated.shrink(1);
|
|
||||||
selfOffer.set(offerSlot, updated);
|
|
||||||
} else {
|
} else {
|
||||||
selfOffer.set(offerSlot, ItemStack.EMPTY);
|
selfOffer.set(offerSlot, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
clearAccepts();
|
clearAccepts();
|
||||||
@@ -189,6 +210,22 @@ public class DebugTradeSession {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setUnsafeState(DebugUnsafeState state, boolean unsafe) {
|
||||||
|
if (unsafe) {
|
||||||
|
unsafeStates.add(state);
|
||||||
|
} else {
|
||||||
|
unsafeStates.remove(state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void clearUnsafeStates() {
|
||||||
|
unsafeStates.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean hasUnsafeState(DebugUnsafeState state) {
|
||||||
|
return unsafeStates.contains(state);
|
||||||
|
}
|
||||||
|
|
||||||
public void advanceToConfirmation() {
|
public void advanceToConfirmation() {
|
||||||
if (stage == TradeStage.OFFERING && selfAccepted && otherAccepted) {
|
if (stage == TradeStage.OFFERING && selfAccepted && otherAccepted) {
|
||||||
stage = TradeStage.CONFIRMING;
|
stage = TradeStage.CONFIRMING;
|
||||||
@@ -206,7 +243,11 @@ public class DebugTradeSession {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public boolean completeTrade() {
|
public boolean completeTrade() {
|
||||||
List<ItemStack> result = inventorySnapshot();
|
List<ItemStack> result = inventorySnapshot(player);
|
||||||
|
if (!removeOutgoing(result, selfOffer)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
for (ItemStack stack : otherOffer) {
|
for (ItemStack stack : otherOffer) {
|
||||||
if (!stack.isEmpty() && !insertStack(result, stack.copy())) {
|
if (!stack.isEmpty() && !insertStack(result, stack.copy())) {
|
||||||
return false;
|
return false;
|
||||||
@@ -230,7 +271,7 @@ public class DebugTradeSession {
|
|||||||
stage,
|
stage,
|
||||||
selfAccepted,
|
selfAccepted,
|
||||||
otherAccepted,
|
otherAccepted,
|
||||||
inventorySnapshot(),
|
inventoryDisplay(),
|
||||||
emptyReservedSnapshot(),
|
emptyReservedSnapshot(),
|
||||||
selfOfferSnapshot(),
|
selfOfferSnapshot(),
|
||||||
otherOfferSnapshot());
|
otherOfferSnapshot());
|
||||||
@@ -310,6 +351,25 @@ public class DebugTradeSession {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static boolean removeOutgoing(List<ItemStack> working, List<TradeEntry> outgoing) {
|
||||||
|
for (TradeEntry entry : outgoing) {
|
||||||
|
if (entry == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
ItemStack current = working.get(entry.sourceSlot());
|
||||||
|
if (!ItemStack.isSameItemSameComponents(current, entry.stack()) || current.getCount() < entry.stack().getCount()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
ItemStack updated = current.copy();
|
||||||
|
updated.shrink(entry.stack().getCount());
|
||||||
|
working.set(entry.sourceSlot(), updated);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
private static boolean insertStack(List<ItemStack> working, ItemStack incoming) {
|
private static boolean insertStack(List<ItemStack> working, ItemStack incoming) {
|
||||||
if (incoming.isEmpty()) {
|
if (incoming.isEmpty()) {
|
||||||
return true;
|
return true;
|
||||||
@@ -348,6 +408,49 @@ public class DebugTradeSession {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private int reservedCount(int inventorySlot) {
|
||||||
|
int count = 0;
|
||||||
|
for (TradeEntry entry : selfOffer) {
|
||||||
|
if (entry != null && entry.sourceSlot() == inventorySlot) {
|
||||||
|
count += entry.stack().getCount();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void clearAccepts() {
|
||||||
|
selfAccepted = false;
|
||||||
|
otherAccepted = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<ItemStack> inventorySnapshotCopy() {
|
||||||
|
List<ItemStack> result = new ArrayList<>(INVENTORY_SLOT_COUNT);
|
||||||
|
for (ItemStack stack : inventorySnapshot) {
|
||||||
|
result.add(stack.copy());
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<ItemStack> inventoryDisplay() {
|
||||||
|
List<ItemStack> result = inventorySnapshotCopy();
|
||||||
|
for (int i = 0; i < INVENTORY_SLOT_COUNT; i++) {
|
||||||
|
int reserved = reservedCount(i);
|
||||||
|
if (reserved <= 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
ItemStack stack = result.get(i);
|
||||||
|
if (stack.isEmpty()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
ItemStack updated = stack.copy();
|
||||||
|
updated.shrink(reserved);
|
||||||
|
result.set(i, updated);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
private static List<Integer> emptyReservedSnapshot() {
|
private static List<Integer> emptyReservedSnapshot() {
|
||||||
List<Integer> result = new ArrayList<>(INVENTORY_SLOT_COUNT);
|
List<Integer> result = new ArrayList<>(INVENTORY_SLOT_COUNT);
|
||||||
for (int i = 0; i < INVENTORY_SLOT_COUNT; i++) {
|
for (int i = 0; i < INVENTORY_SLOT_COUNT; i++) {
|
||||||
@@ -356,12 +459,7 @@ public class DebugTradeSession {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void clearAccepts() {
|
private static List<ItemStack> inventorySnapshot(ServerPlayer player) {
|
||||||
selfAccepted = false;
|
|
||||||
otherAccepted = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<ItemStack> inventorySnapshot() {
|
|
||||||
List<ItemStack> result = new ArrayList<>(INVENTORY_SLOT_COUNT);
|
List<ItemStack> result = new ArrayList<>(INVENTORY_SLOT_COUNT);
|
||||||
Inventory inventory = player.getInventory();
|
Inventory inventory = player.getInventory();
|
||||||
for (int i = 0; i < INVENTORY_SLOT_COUNT; i++) {
|
for (int i = 0; i < INVENTORY_SLOT_COUNT; i++) {
|
||||||
@@ -370,26 +468,10 @@ public class DebugTradeSession {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<ItemStack> selfOfferSnapshot() {
|
private static List<TradeEntry> blankOffer() {
|
||||||
List<ItemStack> result = new ArrayList<>(OFFER_SLOT_COUNT);
|
List<TradeEntry> result = new ArrayList<>(OFFER_SLOT_COUNT);
|
||||||
for (ItemStack stack : selfOffer) {
|
|
||||||
result.add(stack.copy());
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<ItemStack> otherOfferSnapshot() {
|
|
||||||
List<ItemStack> result = new ArrayList<>(OFFER_SLOT_COUNT);
|
|
||||||
for (ItemStack stack : otherOffer) {
|
|
||||||
result.add(stack.copy());
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static List<ItemStack> blankOffer() {
|
|
||||||
List<ItemStack> result = new ArrayList<>(OFFER_SLOT_COUNT);
|
|
||||||
for (int i = 0; i < OFFER_SLOT_COUNT; i++) {
|
for (int i = 0; i < OFFER_SLOT_COUNT; i++) {
|
||||||
result.add(ItemStack.EMPTY);
|
result.add(null);
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
@@ -402,4 +484,12 @@ public class DebugTradeSession {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static int firstFreeOfferSlot(List<TradeEntry> offer) {
|
||||||
|
for (int i = 0; i < offer.size(); i++) {
|
||||||
|
if (offer.get(i) == null) {
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package com.trunksbomb.trade.trade;
|
||||||
|
|
||||||
|
public enum DebugUnsafeState {
|
||||||
|
DAMAGE,
|
||||||
|
FIRE,
|
||||||
|
LIQUID,
|
||||||
|
MOVING,
|
||||||
|
SLEEPING,
|
||||||
|
RIDING,
|
||||||
|
GLIDING
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.trunksbomb.trade.mod.trade;
|
package com.trunksbomb.trade.trade;
|
||||||
|
|
||||||
public enum TradeAction {
|
public enum TradeAction {
|
||||||
ADD_ITEM,
|
ADD_ITEM,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.trunksbomb.trade.mod.trade;
|
package com.trunksbomb.trade.trade;
|
||||||
|
|
||||||
import net.minecraft.world.item.ItemStack;
|
import net.minecraft.world.item.ItemStack;
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,114 @@
|
|||||||
|
package com.trunksbomb.trade.trade;
|
||||||
|
|
||||||
|
import com.mojang.datafixers.util.Pair;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.UUID;
|
||||||
|
import net.minecraft.core.HolderLookup;
|
||||||
|
import net.minecraft.nbt.CompoundTag;
|
||||||
|
import net.minecraft.nbt.ListTag;
|
||||||
|
import net.minecraft.nbt.StringTag;
|
||||||
|
import net.minecraft.nbt.Tag;
|
||||||
|
import net.minecraft.server.MinecraftServer;
|
||||||
|
import net.minecraft.world.level.saveddata.SavedData;
|
||||||
|
|
||||||
|
public class TradePreferencesData extends SavedData {
|
||||||
|
private static final String DATA_NAME = "trade_preferences";
|
||||||
|
|
||||||
|
private final Map<UUID, Boolean> tradeEnabled = new HashMap<>();
|
||||||
|
private final Map<UUID, Set<UUID>> ignoredPlayers = new HashMap<>();
|
||||||
|
|
||||||
|
public static TradePreferencesData get(MinecraftServer server) {
|
||||||
|
return server.overworld().getDataStorage().computeIfAbsent(factory(), DATA_NAME);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Factory<TradePreferencesData> factory() {
|
||||||
|
return new Factory<>(TradePreferencesData::new, TradePreferencesData::load);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static TradePreferencesData load(CompoundTag tag, HolderLookup.Provider registries) {
|
||||||
|
TradePreferencesData data = new TradePreferencesData();
|
||||||
|
|
||||||
|
CompoundTag enabledTag = tag.getCompound("trade_enabled");
|
||||||
|
for (String key : enabledTag.getAllKeys()) {
|
||||||
|
data.tradeEnabled.put(UUID.fromString(key), enabledTag.getBoolean(key));
|
||||||
|
}
|
||||||
|
|
||||||
|
CompoundTag ignoredTag = tag.getCompound("ignored_players");
|
||||||
|
for (String key : ignoredTag.getAllKeys()) {
|
||||||
|
ListTag values = ignoredTag.getList(key, Tag.TAG_STRING);
|
||||||
|
Set<UUID> ignored = new HashSet<>();
|
||||||
|
for (Tag value : values) {
|
||||||
|
ignored.add(UUID.fromString(value.getAsString()));
|
||||||
|
}
|
||||||
|
data.ignoredPlayers.put(UUID.fromString(key), ignored);
|
||||||
|
}
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isTradeEnabled(UUID playerId) {
|
||||||
|
return tradeEnabled.getOrDefault(playerId, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTradeEnabled(UUID playerId, boolean enabled) {
|
||||||
|
tradeEnabled.put(playerId, enabled);
|
||||||
|
setDirty();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isIgnoring(UUID playerId, UUID ignoredPlayerId) {
|
||||||
|
return ignoredPlayers.getOrDefault(playerId, Set.of()).contains(ignoredPlayerId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean addIgnoredPlayer(UUID playerId, UUID ignoredPlayerId) {
|
||||||
|
boolean changed = ignoredPlayers.computeIfAbsent(playerId, ignored -> new HashSet<>()).add(ignoredPlayerId);
|
||||||
|
if (changed) {
|
||||||
|
setDirty();
|
||||||
|
}
|
||||||
|
return changed;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean removeIgnoredPlayer(UUID playerId, UUID ignoredPlayerId) {
|
||||||
|
Set<UUID> ignored = ignoredPlayers.get(playerId);
|
||||||
|
if (ignored == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean changed = ignored.remove(ignoredPlayerId);
|
||||||
|
if (ignored.isEmpty()) {
|
||||||
|
ignoredPlayers.remove(playerId);
|
||||||
|
}
|
||||||
|
if (changed) {
|
||||||
|
setDirty();
|
||||||
|
}
|
||||||
|
return changed;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<UUID> ignoredPlayers(UUID playerId) {
|
||||||
|
return new ArrayList<>(ignoredPlayers.getOrDefault(playerId, Set.of()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public CompoundTag save(CompoundTag tag, HolderLookup.Provider registries) {
|
||||||
|
CompoundTag enabledTag = new CompoundTag();
|
||||||
|
for (Map.Entry<UUID, Boolean> entry : tradeEnabled.entrySet()) {
|
||||||
|
enabledTag.putBoolean(entry.getKey().toString(), entry.getValue());
|
||||||
|
}
|
||||||
|
tag.put("trade_enabled", enabledTag);
|
||||||
|
|
||||||
|
CompoundTag ignoredTag = new CompoundTag();
|
||||||
|
for (Map.Entry<UUID, Set<UUID>> entry : ignoredPlayers.entrySet()) {
|
||||||
|
ListTag list = new ListTag();
|
||||||
|
for (UUID ignored : entry.getValue()) {
|
||||||
|
list.add(StringTag.valueOf(ignored.toString()));
|
||||||
|
}
|
||||||
|
ignoredTag.put(entry.getKey().toString(), list);
|
||||||
|
}
|
||||||
|
tag.put("ignored_players", ignoredTag);
|
||||||
|
return tag;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
package com.trunksbomb.trade.mod.trade;
|
package com.trunksbomb.trade.trade;
|
||||||
|
|
||||||
import com.trunksbomb.trade.mod.network.TradeClosePayload;
|
import com.trunksbomb.trade.network.TradeClosePayload;
|
||||||
import com.trunksbomb.trade.mod.network.TradeStatePayload;
|
import com.trunksbomb.trade.network.TradeStatePayload;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
@@ -10,6 +10,7 @@ import net.minecraft.server.level.ServerPlayer;
|
|||||||
import net.minecraft.world.entity.player.Inventory;
|
import net.minecraft.world.entity.player.Inventory;
|
||||||
import net.minecraft.world.item.ItemStack;
|
import net.minecraft.world.item.ItemStack;
|
||||||
import net.neoforged.neoforge.network.PacketDistributor;
|
import net.neoforged.neoforge.network.PacketDistributor;
|
||||||
|
import com.trunksbomb.trade.mod.TradeConfig;
|
||||||
|
|
||||||
public class TradeSession {
|
public class TradeSession {
|
||||||
private static final int INVENTORY_SLOT_COUNT = TradeView.INVENTORY_SLOT_COUNT;
|
private static final int INVENTORY_SLOT_COUNT = TradeView.INVENTORY_SLOT_COUNT;
|
||||||
@@ -18,8 +19,10 @@ public class TradeSession {
|
|||||||
private final UUID id = UUID.randomUUID();
|
private final UUID id = UUID.randomUUID();
|
||||||
private final ServerPlayer first;
|
private final ServerPlayer first;
|
||||||
private final ServerPlayer second;
|
private final ServerPlayer second;
|
||||||
private final List<ItemStack> firstOffer = blankOffer();
|
private final List<ItemStack> firstInventory;
|
||||||
private final List<ItemStack> secondOffer = blankOffer();
|
private final List<ItemStack> secondInventory;
|
||||||
|
private final List<TradeEntry> firstOffer = blankOffer();
|
||||||
|
private final List<TradeEntry> secondOffer = blankOffer();
|
||||||
private boolean firstAccepted;
|
private boolean firstAccepted;
|
||||||
private boolean secondAccepted;
|
private boolean secondAccepted;
|
||||||
private TradeStage stage = TradeStage.OFFERING;
|
private TradeStage stage = TradeStage.OFFERING;
|
||||||
@@ -27,6 +30,8 @@ public class TradeSession {
|
|||||||
public TradeSession(ServerPlayer first, ServerPlayer second) {
|
public TradeSession(ServerPlayer first, ServerPlayer second) {
|
||||||
this.first = first;
|
this.first = first;
|
||||||
this.second = second;
|
this.second = second;
|
||||||
|
this.firstInventory = inventorySnapshot(first);
|
||||||
|
this.secondInventory = inventorySnapshot(second);
|
||||||
}
|
}
|
||||||
|
|
||||||
public UUID id() {
|
public UUID id() {
|
||||||
@@ -41,6 +46,14 @@ public class TradeSession {
|
|||||||
return second;
|
return second;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public List<ItemStack> firstOfferSnapshot() {
|
||||||
|
return offerSnapshot(firstOffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<ItemStack> secondOfferSnapshot() {
|
||||||
|
return offerSnapshot(secondOffer);
|
||||||
|
}
|
||||||
|
|
||||||
public boolean involves(ServerPlayer player) {
|
public boolean involves(ServerPlayer player) {
|
||||||
return player == first || player == second;
|
return player == first || player == second;
|
||||||
}
|
}
|
||||||
@@ -59,58 +72,64 @@ public class TradeSession {
|
|||||||
PacketDistributor.sendToPlayer(second, new TradeClosePayload(id, reason));
|
PacketDistributor.sendToPlayer(second, new TradeClosePayload(id, reason));
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean addFromInventory(ServerPlayer player, int inventorySlot, boolean singleItem) {
|
public boolean addFromInventory(ServerPlayer player, int inventorySlot, int amount) {
|
||||||
if (stage != TradeStage.OFFERING || inventorySlot < 0 || inventorySlot >= INVENTORY_SLOT_COUNT) {
|
if (stage != TradeStage.OFFERING || inventorySlot < 0 || inventorySlot >= INVENTORY_SLOT_COUNT) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
List<ItemStack> offer = offerFor(player);
|
ItemStack sourceStack = inventoryFor(player).get(inventorySlot);
|
||||||
Inventory inventory = player.getInventory();
|
|
||||||
ItemStack sourceStack = inventory.getItem(inventorySlot);
|
|
||||||
if (sourceStack.isEmpty()) {
|
if (sourceStack.isEmpty()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
ItemStack moving = sourceStack.copyWithCount(singleItem ? 1 : sourceStack.getCount());
|
int available = sourceStack.getCount() - reservedCount(player, inventorySlot);
|
||||||
if (!insertStack(offer, moving)) {
|
if (available <= 0) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
sourceStack.shrink(singleItem ? 1 : sourceStack.getCount());
|
int moveAmount = Math.max(1, Math.min(amount, available));
|
||||||
if (sourceStack.isEmpty()) {
|
List<TradeEntry> offer = offerFor(player);
|
||||||
inventory.setItem(inventorySlot, ItemStack.EMPTY);
|
for (int i = 0; i < offer.size(); i++) {
|
||||||
|
TradeEntry entry = offer.get(i);
|
||||||
|
if (entry != null && entry.sourceSlot() == inventorySlot && ItemStack.isSameItemSameComponents(entry.stack(), sourceStack)) {
|
||||||
|
ItemStack merged = entry.stack().copy();
|
||||||
|
merged.grow(moveAmount);
|
||||||
|
offer.set(i, new TradeEntry(inventorySlot, merged));
|
||||||
|
clearAccepts();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
inventory.setChanged();
|
|
||||||
|
int freeSlot = firstFreeOfferSlot(offer);
|
||||||
|
if (freeSlot == -1) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
offer.set(freeSlot, new TradeEntry(inventorySlot, sourceStack.copyWithCount(moveAmount)));
|
||||||
clearAccepts();
|
clearAccepts();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean removeFromOffer(ServerPlayer player, int offerSlot, boolean singleItem) {
|
public boolean removeFromOffer(ServerPlayer player, int offerSlot, int amount) {
|
||||||
if (stage != TradeStage.OFFERING || offerSlot < 0 || offerSlot >= OFFER_SLOT_COUNT) {
|
if (stage != TradeStage.OFFERING || offerSlot < 0 || offerSlot >= OFFER_SLOT_COUNT) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
List<ItemStack> offer = offerFor(player);
|
List<TradeEntry> offer = offerFor(player);
|
||||||
ItemStack offered = offer.get(offerSlot);
|
TradeEntry entry = offer.get(offerSlot);
|
||||||
if (offered.isEmpty()) {
|
if (entry == null) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
Inventory inventory = player.getInventory();
|
int moveAmount = Math.max(1, Math.min(amount, entry.stack().getCount()));
|
||||||
List<ItemStack> workingInventory = inventorySnapshot(player);
|
if (moveAmount < entry.stack().getCount()) {
|
||||||
ItemStack returned = offered.copyWithCount(singleItem ? 1 : offered.getCount());
|
ItemStack updated = entry.stack().copy();
|
||||||
if (!insertStack(workingInventory, returned)) {
|
updated.shrink(moveAmount);
|
||||||
return false;
|
offer.set(offerSlot, new TradeEntry(entry.sourceSlot(), updated));
|
||||||
}
|
|
||||||
|
|
||||||
applyInventory(player, workingInventory);
|
|
||||||
if (singleItem && offered.getCount() > 1) {
|
|
||||||
ItemStack updated = offered.copy();
|
|
||||||
updated.shrink(1);
|
|
||||||
offer.set(offerSlot, updated);
|
|
||||||
} else {
|
} else {
|
||||||
offer.set(offerSlot, ItemStack.EMPTY);
|
offer.set(offerSlot, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
clearAccepts();
|
clearAccepts();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -139,18 +158,36 @@ public class TradeSession {
|
|||||||
return stage == TradeStage.CONFIRMING;
|
return stage == TradeStage.CONFIRMING;
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean completeTrade() {
|
public CompletionResult completeTrade() {
|
||||||
List<ItemStack> firstResult = simulateResult(first, secondOffer);
|
if (first.level() != second.level()) {
|
||||||
List<ItemStack> secondResult = simulateResult(second, firstOffer);
|
return CompletionResult.failure(Component.literal("Trade cancelled because both players must remain in the same dimension."));
|
||||||
|
}
|
||||||
|
|
||||||
|
int maxDistance = TradeConfig.tradeCommandProximity();
|
||||||
|
if (maxDistance > 0) {
|
||||||
|
double maxDistanceSquared = maxDistance * maxDistance;
|
||||||
|
if (first.distanceToSqr(second) > maxDistanceSquared) {
|
||||||
|
return CompletionResult.failure(Component.literal("Trade cancelled because players moved too far apart before it was finalized."));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!inventoryMatchesSnapshot(first, firstInventory)) {
|
||||||
|
return CompletionResult.failure(Component.literal("Trade cancelled because " + first.getGameProfile().getName() + "'s inventory changed during the trade."));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!inventoryMatchesSnapshot(second, secondInventory)) {
|
||||||
|
return CompletionResult.failure(Component.literal("Trade cancelled because " + second.getGameProfile().getName() + "'s inventory changed during the trade."));
|
||||||
|
}
|
||||||
|
|
||||||
|
List<ItemStack> firstResult = simulateResult(first, firstOffer, secondOffer);
|
||||||
|
List<ItemStack> secondResult = simulateResult(second, secondOffer, firstOffer);
|
||||||
if (firstResult == null || secondResult == null) {
|
if (firstResult == null || secondResult == null) {
|
||||||
return false;
|
return CompletionResult.failure(Component.literal("Trade cancelled because one player could not fit all traded items."));
|
||||||
}
|
}
|
||||||
|
|
||||||
applyInventory(first, firstResult);
|
applyInventory(first, firstResult);
|
||||||
applyInventory(second, secondResult);
|
applyInventory(second, secondResult);
|
||||||
clearOffer(firstOffer);
|
return CompletionResult.success();
|
||||||
clearOffer(secondOffer);
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public TradeView viewFor(ServerPlayer player) {
|
public TradeView viewFor(ServerPlayer player) {
|
||||||
@@ -164,7 +201,7 @@ public class TradeSession {
|
|||||||
stage,
|
stage,
|
||||||
isFirst ? firstAccepted : secondAccepted,
|
isFirst ? firstAccepted : secondAccepted,
|
||||||
isFirst ? secondAccepted : firstAccepted,
|
isFirst ? secondAccepted : firstAccepted,
|
||||||
inventorySnapshot(player),
|
inventoryDisplayFor(player),
|
||||||
emptyReservedSnapshot(),
|
emptyReservedSnapshot(),
|
||||||
offerSnapshot(isFirst ? firstOffer : secondOffer),
|
offerSnapshot(isFirst ? firstOffer : secondOffer),
|
||||||
offerSnapshot(isFirst ? secondOffer : firstOffer));
|
offerSnapshot(isFirst ? secondOffer : firstOffer));
|
||||||
@@ -174,10 +211,14 @@ public class TradeSession {
|
|||||||
PacketDistributor.sendToPlayer(player, new TradeStatePayload(viewFor(player)));
|
PacketDistributor.sendToPlayer(player, new TradeStatePayload(viewFor(player)));
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<ItemStack> simulateResult(ServerPlayer player, List<ItemStack> incoming) {
|
private List<ItemStack> simulateResult(ServerPlayer player, List<TradeEntry> outgoing, List<TradeEntry> incoming) {
|
||||||
List<ItemStack> working = inventorySnapshot(player);
|
List<ItemStack> working = inventorySnapshot(player);
|
||||||
for (ItemStack stack : incoming) {
|
if (!removeOutgoing(working, outgoing)) {
|
||||||
if (!stack.isEmpty() && !insertStack(working, stack.copy())) {
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (TradeEntry entry : incoming) {
|
||||||
|
if (entry != null && !insertStack(working, entry.stack().copy())) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -185,6 +226,25 @@ public class TradeSession {
|
|||||||
return working;
|
return working;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static boolean removeOutgoing(List<ItemStack> working, List<TradeEntry> outgoing) {
|
||||||
|
for (TradeEntry entry : outgoing) {
|
||||||
|
if (entry == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
ItemStack current = working.get(entry.sourceSlot());
|
||||||
|
if (!ItemStack.isSameItemSameComponents(current, entry.stack()) || current.getCount() < entry.stack().getCount()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
ItemStack updated = current.copy();
|
||||||
|
updated.shrink(entry.stack().getCount());
|
||||||
|
working.set(entry.sourceSlot(), updated);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
private static boolean insertStack(List<ItemStack> working, ItemStack incoming) {
|
private static boolean insertStack(List<ItemStack> working, ItemStack incoming) {
|
||||||
if (incoming.isEmpty()) {
|
if (incoming.isEmpty()) {
|
||||||
return true;
|
return true;
|
||||||
@@ -223,12 +283,6 @@ public class TradeSession {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void clearOffer(List<ItemStack> offer) {
|
|
||||||
for (int i = 0; i < offer.size(); i++) {
|
|
||||||
offer.set(i, ItemStack.EMPTY);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void applyInventory(ServerPlayer player, List<ItemStack> result) {
|
private static void applyInventory(ServerPlayer player, List<ItemStack> result) {
|
||||||
Inventory inventory = player.getInventory();
|
Inventory inventory = player.getInventory();
|
||||||
for (int i = 0; i < result.size(); i++) {
|
for (int i = 0; i < result.size(); i++) {
|
||||||
@@ -237,19 +291,84 @@ public class TradeSession {
|
|||||||
inventory.setChanged();
|
inventory.setChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static List<ItemStack> blankOffer() {
|
private static boolean inventoryMatchesSnapshot(ServerPlayer player, List<ItemStack> snapshot) {
|
||||||
List<ItemStack> result = new ArrayList<>(OFFER_SLOT_COUNT);
|
Inventory inventory = player.getInventory();
|
||||||
|
for (int i = 0; i < INVENTORY_SLOT_COUNT; i++) {
|
||||||
|
if (!ItemStack.matches(inventory.getItem(i), snapshot.get(i))) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<TradeEntry> blankOffer() {
|
||||||
|
List<TradeEntry> result = new ArrayList<>(OFFER_SLOT_COUNT);
|
||||||
for (int i = 0; i < OFFER_SLOT_COUNT; i++) {
|
for (int i = 0; i < OFFER_SLOT_COUNT; i++) {
|
||||||
result.add(ItemStack.EMPTY);
|
result.add(null);
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static int firstFreeOfferSlot(List<TradeEntry> offer) {
|
||||||
|
for (int i = 0; i < offer.size(); i++) {
|
||||||
|
if (offer.get(i) == null) {
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
private void clearAccepts() {
|
private void clearAccepts() {
|
||||||
firstAccepted = false;
|
firstAccepted = false;
|
||||||
secondAccepted = false;
|
secondAccepted = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private int reservedCount(ServerPlayer player, int inventorySlot) {
|
||||||
|
int count = 0;
|
||||||
|
for (TradeEntry entry : offerFor(player)) {
|
||||||
|
if (entry != null && entry.sourceSlot() == inventorySlot) {
|
||||||
|
count += entry.stack().getCount();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<TradeEntry> offerFor(ServerPlayer player) {
|
||||||
|
return player == first ? firstOffer : secondOffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<ItemStack> inventoryFor(ServerPlayer player) {
|
||||||
|
return player == first ? firstInventory : secondInventory;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<ItemStack> inventorySnapshotFor(ServerPlayer player) {
|
||||||
|
List<ItemStack> result = new ArrayList<>(INVENTORY_SLOT_COUNT);
|
||||||
|
for (ItemStack stack : inventoryFor(player)) {
|
||||||
|
result.add(stack.copy());
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<ItemStack> inventoryDisplayFor(ServerPlayer player) {
|
||||||
|
List<ItemStack> result = inventorySnapshotFor(player);
|
||||||
|
for (int i = 0; i < INVENTORY_SLOT_COUNT; i++) {
|
||||||
|
int reserved = reservedCount(player, i);
|
||||||
|
if (reserved <= 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
ItemStack stack = result.get(i);
|
||||||
|
if (stack.isEmpty()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
ItemStack updated = stack.copy();
|
||||||
|
updated.shrink(reserved);
|
||||||
|
result.set(i, updated);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
private static List<Integer> emptyReservedSnapshot() {
|
private static List<Integer> emptyReservedSnapshot() {
|
||||||
List<Integer> result = new ArrayList<>(INVENTORY_SLOT_COUNT);
|
List<Integer> result = new ArrayList<>(INVENTORY_SLOT_COUNT);
|
||||||
for (int i = 0; i < INVENTORY_SLOT_COUNT; i++) {
|
for (int i = 0; i < INVENTORY_SLOT_COUNT; i++) {
|
||||||
@@ -258,10 +377,6 @@ public class TradeSession {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<ItemStack> offerFor(ServerPlayer player) {
|
|
||||||
return player == first ? firstOffer : secondOffer;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static List<ItemStack> inventorySnapshot(ServerPlayer player) {
|
private static List<ItemStack> inventorySnapshot(ServerPlayer player) {
|
||||||
List<ItemStack> result = new ArrayList<>(INVENTORY_SLOT_COUNT);
|
List<ItemStack> result = new ArrayList<>(INVENTORY_SLOT_COUNT);
|
||||||
Inventory inventory = player.getInventory();
|
Inventory inventory = player.getInventory();
|
||||||
@@ -271,11 +386,21 @@ public class TradeSession {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static List<ItemStack> offerSnapshot(List<ItemStack> offer) {
|
private static List<ItemStack> offerSnapshot(List<TradeEntry> offer) {
|
||||||
List<ItemStack> result = new ArrayList<>(OFFER_SLOT_COUNT);
|
List<ItemStack> result = new ArrayList<>(OFFER_SLOT_COUNT);
|
||||||
for (ItemStack stack : offer) {
|
for (TradeEntry entry : offer) {
|
||||||
result.add(stack.copy());
|
result.add(entry == null ? ItemStack.EMPTY : entry.stack().copy());
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public record CompletionResult(boolean successful, Component failureReason) {
|
||||||
|
public static CompletionResult success() {
|
||||||
|
return new CompletionResult(true, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static CompletionResult failure(Component failureReason) {
|
||||||
|
return new CompletionResult(false, failureReason);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.trunksbomb.trade.mod.trade;
|
package com.trunksbomb.trade.trade;
|
||||||
|
|
||||||
public enum TradeStage {
|
public enum TradeStage {
|
||||||
OFFERING,
|
OFFERING,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.trunksbomb.trade.mod.trade;
|
package com.trunksbomb.trade.trade;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
{
|
{
|
||||||
|
"key.trade.request_trade": "Request Trade",
|
||||||
"trade.trade.title": "Trading Screen",
|
"trade.trade.title": "Trading Screen",
|
||||||
"trade.trade.offer": "Offer screen",
|
"trade.trade.offer": "Offer screen",
|
||||||
"trade.trade.confirm": "Confirmation screen"
|
"trade.trade.confirm": "Confirmation screen"
|
||||||
|
|||||||
Reference in New Issue
Block a user