Add initial implementation of SPMega mod with banking UI and API integration
Java CI / build (push) Successful in 5m38s
Java CI / build (push) Successful in 5m38s
Signed-off-by: Dmitrii <computer@yawaflua.tech> Took 2 hours 53 minutes
This commit is contained in:
@@ -0,0 +1,89 @@
|
||||
package git.yawaflua.tech.spmega.client;
|
||||
|
||||
import git.yawaflua.tech.spmega.SPMega;
|
||||
import git.yawaflua.tech.spmega.client.ui.UiOpeners;
|
||||
import git.yawaflua.tech.spmega.client.ui.service.BankUiService;
|
||||
import net.fabricmc.api.ClientModInitializer;
|
||||
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
|
||||
import net.fabricmc.fabric.api.client.keybinding.v1.KeyBindingHelper;
|
||||
import net.fabricmc.fabric.api.client.networking.v1.ClientPlayConnectionEvents;
|
||||
import net.fabricmc.fabric.api.event.player.UseBlockCallback;
|
||||
import net.minecraft.block.entity.SignBlockEntity;
|
||||
import net.minecraft.client.MinecraftClient;
|
||||
import net.minecraft.client.option.KeyBinding;
|
||||
import net.minecraft.client.util.InputUtil;
|
||||
import net.minecraft.text.Text;
|
||||
import net.minecraft.util.ActionResult;
|
||||
import org.lwjgl.glfw.GLFW;
|
||||
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public class SPMegaClient implements ClientModInitializer {
|
||||
private static final Pattern ISOLATED_FIVE_DIGITS = Pattern.compile("(?<!\\d)(\\d{5})(?!\\d)");
|
||||
private static KeyBinding openBankMenuKeyBinding;
|
||||
|
||||
private static String extractCardNumber(SignBlockEntity signBlockEntity) {
|
||||
String candidate = findFiveDigits(signBlockEntity.getFrontText().getMessages(false));
|
||||
if (candidate != null) {
|
||||
return candidate;
|
||||
}
|
||||
return findFiveDigits(signBlockEntity.getBackText().getMessages(false));
|
||||
}
|
||||
|
||||
private static String findFiveDigits(Text[] lines) {
|
||||
for (Text line : lines) {
|
||||
Matcher matcher = ISOLATED_FIVE_DIGITS.matcher(line.getString());
|
||||
if (matcher.find()) {
|
||||
return matcher.group(1);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onInitializeClient() {
|
||||
openBankMenuKeyBinding = KeyBindingHelper.registerKeyBinding(new KeyBinding(
|
||||
"key.spmega.open_menu",
|
||||
InputUtil.Type.KEYSYM,
|
||||
GLFW.GLFW_KEY_P,
|
||||
KeyBinding.Category.GAMEPLAY
|
||||
));
|
||||
|
||||
ClientTickEvents.END_CLIENT_TICK.register(client -> {
|
||||
while (openBankMenuKeyBinding.wasPressed()) {
|
||||
UiOpeners.openMainMenu(client);
|
||||
}
|
||||
});
|
||||
|
||||
ClientPlayConnectionEvents.JOIN.register((handler, sender, client) -> {
|
||||
if (client.player != null) {
|
||||
BankUiService.instance().refreshOnServerJoin(client.player.getUuidAsString());
|
||||
}
|
||||
});
|
||||
|
||||
UseBlockCallback.EVENT.register((player, world, hand, hitResult) -> {
|
||||
if (!world.isClient()) {
|
||||
return ActionResult.PASS;
|
||||
}
|
||||
if (!player.isSneaking()) {
|
||||
return ActionResult.PASS;
|
||||
}
|
||||
if (SPMega.getConfig() == null || !SPMega.getConfig().signQuickPayEnabled()) {
|
||||
return ActionResult.PASS;
|
||||
}
|
||||
|
||||
if (!(world.getBlockEntity(hitResult.getBlockPos()) instanceof SignBlockEntity signBlockEntity)) {
|
||||
return ActionResult.PASS;
|
||||
}
|
||||
|
||||
String cardNumber = extractCardNumber(signBlockEntity);
|
||||
if (cardNumber == null) {
|
||||
return ActionResult.PASS;
|
||||
}
|
||||
|
||||
UiOpeners.openPaymentMenu(MinecraftClient.getInstance(), cardNumber);
|
||||
return ActionResult.SUCCESS;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package git.yawaflua.tech.spmega.client;
|
||||
|
||||
import net.fabricmc.fabric.api.datagen.v1.DataGeneratorEntrypoint;
|
||||
import net.fabricmc.fabric.api.datagen.v1.FabricDataGenerator;
|
||||
|
||||
public class SPMegaDataGenerator implements DataGeneratorEntrypoint {
|
||||
|
||||
@Override
|
||||
public void onInitializeDataGenerator(FabricDataGenerator fabricDataGenerator) {
|
||||
FabricDataGenerator.Pack pack = fabricDataGenerator.createPack();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
package git.yawaflua.tech.spmega.client.ui;
|
||||
|
||||
import git.yawaflua.tech.spmega.client.ui.service.BankUiService;
|
||||
import net.minecraft.client.gui.DrawContext;
|
||||
import net.minecraft.client.gui.screen.Screen;
|
||||
import net.minecraft.client.gui.widget.ButtonWidget;
|
||||
import net.minecraft.client.gui.widget.TextFieldWidget;
|
||||
import net.minecraft.text.Text;
|
||||
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
public class AddCardScreen extends Screen {
|
||||
private final Screen parent;
|
||||
private final Consumer<String> onSubmitMessage;
|
||||
private final BankUiService bankUiService = BankUiService.instance();
|
||||
private final UiNotifications notifications = UiNotifications.instance();
|
||||
private TextFieldWidget cardIdField;
|
||||
private TextFieldWidget cardTokenField;
|
||||
private ButtonWidget addButton;
|
||||
private ButtonWidget cancelButton;
|
||||
|
||||
public AddCardScreen(Screen parent, Consumer<String> onSubmitMessage) {
|
||||
super(Text.literal("Добавление карты"));
|
||||
this.parent = parent;
|
||||
this.onSubmitMessage = onSubmitMessage;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void init() {
|
||||
int centerX = this.width / 2;
|
||||
int startY = this.height / 2 - 50;
|
||||
|
||||
cardIdField = new TextFieldWidget(this.textRenderer, centerX - 140, startY, 280, 20, Text.literal("Card ID"));
|
||||
cardIdField.setMaxLength(64);
|
||||
cardIdField.setPlaceholder(Text.literal("ID карты (UUID)"));
|
||||
this.addDrawableChild(cardIdField);
|
||||
|
||||
cardTokenField = new TextFieldWidget(this.textRenderer, centerX - 140, startY + 28, 280, 20, Text.literal("Card Token"));
|
||||
cardTokenField.setMaxLength(128);
|
||||
cardTokenField.setPlaceholder(Text.literal("Токен карты"));
|
||||
this.addDrawableChild(cardTokenField);
|
||||
|
||||
addButton = this.addDrawableChild(ButtonWidget.builder(Text.literal("Добавить"), button -> submit())
|
||||
.dimensions(centerX - 140, startY + 60, 136, 20)
|
||||
.build());
|
||||
|
||||
cancelButton = this.addDrawableChild(ButtonWidget.builder(Text.literal("Отмена"), button -> this.close())
|
||||
.dimensions(centerX + 4, startY + 60, 136, 20)
|
||||
.build());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
if (this.client != null) {
|
||||
this.client.setScreen(parent);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void render(DrawContext context, int mouseX, int mouseY, float delta) {
|
||||
super.render(context, mouseX, mouseY, delta);
|
||||
|
||||
int centerX = this.width / 2;
|
||||
int startY = this.height / 2 - 50;
|
||||
|
||||
context.drawCenteredTextWithShadow(this.textRenderer, this.title, centerX, 24, 0xFFFFFF);
|
||||
context.drawTextWithShadow(this.textRenderer, Text.literal("Card ID (UUID):"), centerX - 140, startY - 10, 0xCCCCCC);
|
||||
context.drawTextWithShadow(this.textRenderer, Text.literal("Card Token:"), centerX - 140, startY + 18, 0xCCCCCC);
|
||||
notifications.render(context, this.textRenderer, this.width, this.height);
|
||||
}
|
||||
|
||||
private void submit() {
|
||||
String cardId = cardIdField.getText().trim();
|
||||
String cardToken = cardTokenField.getText().trim();
|
||||
if (cardId.isEmpty() || cardToken.isEmpty()) {
|
||||
notifications.show(Text.literal("Укажи cardId и cardToken"));
|
||||
return;
|
||||
}
|
||||
|
||||
String playerUuid = this.client != null && this.client.player != null
|
||||
? this.client.player.getUuidAsString()
|
||||
: "";
|
||||
|
||||
notifications.show(Text.literal("Проверка карты..."));
|
||||
addButton.active = false;
|
||||
cancelButton.active = false;
|
||||
|
||||
CompletableFuture
|
||||
.supplyAsync(() -> bankUiService.addCard(cardId, cardToken, playerUuid))
|
||||
.thenAccept(message -> {
|
||||
if (this.client == null) {
|
||||
return;
|
||||
}
|
||||
this.client.execute(() -> {
|
||||
notifications.showMessage(message);
|
||||
addButton.active = true;
|
||||
cancelButton.active = true;
|
||||
|
||||
if (onSubmitMessage != null) {
|
||||
onSubmitMessage.accept(message);
|
||||
}
|
||||
|
||||
if (message.startsWith("Карта добавлена") || message.startsWith("Вы не владелец карты")) {
|
||||
this.close();
|
||||
}
|
||||
});
|
||||
})
|
||||
.exceptionally(exception -> {
|
||||
if (this.client != null) {
|
||||
this.client.execute(() -> {
|
||||
notifications.show(Text.literal("Ошибка добавления карты: " + exception.getMessage()));
|
||||
addButton.active = true;
|
||||
cancelButton.active = true;
|
||||
});
|
||||
}
|
||||
return null;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
package git.yawaflua.tech.spmega.client.ui;
|
||||
|
||||
import git.yawaflua.tech.spmega.client.ui.service.BankUiService;
|
||||
import git.yawaflua.tech.spmega.client.ui.service.CardViewModel;
|
||||
import net.minecraft.client.gui.DrawContext;
|
||||
import net.minecraft.client.gui.screen.Screen;
|
||||
import net.minecraft.client.gui.widget.ButtonWidget;
|
||||
import net.minecraft.text.Text;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class CardScreen extends Screen {
|
||||
private final Screen parent;
|
||||
private final BankUiService bankUiService = BankUiService.instance();
|
||||
private final UiNotifications notifications = UiNotifications.instance();
|
||||
private final List<ButtonWidget> cardButtons = new ArrayList<>();
|
||||
|
||||
public CardScreen(Screen parent) {
|
||||
super(Text.literal("Управление картами"));
|
||||
this.parent = parent;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void init() {
|
||||
cardButtons.clear();
|
||||
|
||||
int centerX = this.width / 2;
|
||||
int startY = this.height / 2 - 82;
|
||||
int leftX = centerX - 210;
|
||||
int rightX = centerX + 14;
|
||||
int columnWidth = 196;
|
||||
|
||||
List<CardViewModel> cards = bankUiService.getCards();
|
||||
for (int i = 0; i < cards.size(); i++) {
|
||||
final int index = i;
|
||||
CardViewModel card = cards.get(i);
|
||||
ButtonWidget cardButton = this.addDrawableChild(ButtonWidget.builder(Text.literal(card.title()), button -> {
|
||||
bankUiService.setSelectedCardIndex(index);
|
||||
notifications.show(Text.literal("Выбрана карта " + card.title()));
|
||||
updateCardButtonStates();
|
||||
}).dimensions(leftX, startY + i * 24, columnWidth, 20).build());
|
||||
cardButtons.add(cardButton);
|
||||
}
|
||||
|
||||
this.addDrawableChild(ButtonWidget.builder(Text.literal("Удалить"), button -> {
|
||||
bankUiService.removeSelectedCard();
|
||||
notifications.showMessage(bankUiService.getLastMessage());
|
||||
this.clearAndInit();
|
||||
}).dimensions(rightX, startY, columnWidth, 20).build());
|
||||
|
||||
this.addDrawableChild(ButtonWidget.builder(Text.literal("Изменить"), button -> {
|
||||
String playerUuid = this.client != null && this.client.player != null
|
||||
? this.client.player.getUuidAsString()
|
||||
: "";
|
||||
bankUiService.refreshSelectedCard(playerUuid);
|
||||
String message = bankUiService.getLastMessage().isBlank() ? "Карта обновлена" : bankUiService.getLastMessage();
|
||||
notifications.showMessage(message);
|
||||
this.clearAndInit();
|
||||
}).dimensions(rightX, startY + 24, columnWidth, 20).build());
|
||||
|
||||
this.addDrawableChild(ButtonWidget.builder(Text.literal("Добавить новую"), button -> {
|
||||
if (this.client == null) {
|
||||
return;
|
||||
}
|
||||
this.client.setScreen(new AddCardScreen(this, message -> {
|
||||
notifications.showMessage(message);
|
||||
this.clearAndInit();
|
||||
}));
|
||||
}).dimensions(rightX, startY + 48, columnWidth, 20).build());
|
||||
|
||||
this.addDrawableChild(ButtonWidget.builder(Text.literal("Назад"), button -> this.close())
|
||||
.dimensions(rightX, startY + 120, columnWidth, 20)
|
||||
.build());
|
||||
|
||||
if (cards.isEmpty()) {
|
||||
notifications.show(Text.literal("Нет карт. Добавь карту через кнопку 'Добавить новую'."));
|
||||
}
|
||||
|
||||
updateCardButtonStates();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
if (this.client != null) {
|
||||
this.client.setScreen(parent);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void render(DrawContext context, int mouseX, int mouseY, float delta) {
|
||||
super.render(context, mouseX, mouseY, delta);
|
||||
|
||||
int centerX = this.width / 2;
|
||||
int startY = this.height / 2 - 82;
|
||||
int leftX = centerX - 210;
|
||||
int rightX = centerX + 14;
|
||||
|
||||
context.drawCenteredTextWithShadow(this.textRenderer, this.title, centerX, 24, 0xFFFFFF);
|
||||
context.drawTextWithShadow(this.textRenderer, Text.literal("Список карт"), leftX, startY - 18, 0xBFBFBF);
|
||||
context.drawTextWithShadow(this.textRenderer, Text.literal("Действия"), rightX, startY - 18, 0xBFBFBF);
|
||||
notifications.render(context, this.textRenderer, this.width, this.height);
|
||||
}
|
||||
|
||||
private void updateCardButtonStates() {
|
||||
int selectedIndex = bankUiService.getSelectedCardIndex();
|
||||
List<CardViewModel> cards = bankUiService.getCards();
|
||||
|
||||
for (int i = 0; i < cardButtons.size(); i++) {
|
||||
ButtonWidget button = cardButtons.get(i);
|
||||
if (i >= cards.size()) {
|
||||
button.visible = false;
|
||||
button.active = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
boolean selected = i == selectedIndex;
|
||||
String prefix = selected ? ">> " : "";
|
||||
String suffix = selected ? " <<" : "";
|
||||
button.setMessage(Text.literal(prefix + cards.get(i).title() + suffix));
|
||||
button.active = !selected;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package git.yawaflua.tech.spmega.client.ui;
|
||||
|
||||
import net.minecraft.client.MinecraftClient;
|
||||
import net.minecraft.client.gui.DrawContext;
|
||||
import net.minecraft.client.gui.screen.Screen;
|
||||
import net.minecraft.client.gui.widget.ButtonWidget;
|
||||
import net.minecraft.text.Text;
|
||||
|
||||
public class MainBankScreen extends Screen {
|
||||
private final Screen parent;
|
||||
|
||||
public MainBankScreen(Screen parent) {
|
||||
super(Text.literal("SPMega"));
|
||||
this.parent = parent;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void init() {
|
||||
int centerX = this.width / 2;
|
||||
int y = this.height / 2 + 4;
|
||||
int buttonWidth = 92;
|
||||
int gap = 8;
|
||||
int totalWidth = buttonWidth * 3 + gap * 2;
|
||||
int startX = centerX - totalWidth / 2;
|
||||
|
||||
this.addDrawableChild(ButtonWidget.builder(Text.literal("Карта"), button -> {
|
||||
this.client.setScreen(new CardScreen(this));
|
||||
}).dimensions(startX, y, buttonWidth, 20).build());
|
||||
|
||||
this.addDrawableChild(ButtonWidget.builder(Text.literal("Оплата"), button -> {
|
||||
this.client.setScreen(new PaymentScreen(this));
|
||||
}).dimensions(startX + buttonWidth + gap, y, buttonWidth, 20).build());
|
||||
|
||||
this.addDrawableChild(ButtonWidget.builder(Text.literal("Закрыть"), button -> this.close())
|
||||
.dimensions(startX + (buttonWidth + gap) * 2, y, buttonWidth, 20)
|
||||
.build());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
if (this.client != null) {
|
||||
this.client.setScreen(parent);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void render(DrawContext context, int mouseX, int mouseY, float delta) {
|
||||
super.render(context, mouseX, mouseY, delta);
|
||||
|
||||
context.drawCenteredTextWithShadow(this.textRenderer, this.title, this.width / 2, 24, 0xFFFFFF);
|
||||
context.drawCenteredTextWithShadow(this.textRenderer, Text.literal(greetingLine()), this.width / 2, 48, 0xFFD27F);
|
||||
}
|
||||
|
||||
private String greetingLine() {
|
||||
MinecraftClient minecraftClient = MinecraftClient.getInstance();
|
||||
String username = minecraftClient.getSession().getUsername();
|
||||
return "Доброе " + getTimeOfDay() + ", " + username;
|
||||
}
|
||||
|
||||
private String getTimeOfDay() {
|
||||
MinecraftClient minecraftClient = MinecraftClient.getInstance();
|
||||
if (minecraftClient.world == null) {
|
||||
return "время суток";
|
||||
}
|
||||
|
||||
long dayTime = minecraftClient.world.getTimeOfDay() % 24000L;
|
||||
if (dayTime < 6000) {
|
||||
return "утро";
|
||||
}
|
||||
if (dayTime < 12000) {
|
||||
return "день";
|
||||
}
|
||||
if (dayTime < 18000) {
|
||||
return "вечер";
|
||||
}
|
||||
return "ночь";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,384 @@
|
||||
package git.yawaflua.tech.spmega.client.ui;
|
||||
|
||||
import git.yawaflua.tech.spmega.client.ui.service.BankUiService;
|
||||
import git.yawaflua.tech.spmega.client.ui.service.CardViewModel;
|
||||
import git.yawaflua.tech.spmega.client.ui.service.PaymentDraft;
|
||||
import net.minecraft.client.font.TextRenderer;
|
||||
import net.minecraft.client.gui.DrawContext;
|
||||
import net.minecraft.client.gui.screen.Screen;
|
||||
import net.minecraft.client.gui.widget.ButtonWidget;
|
||||
import net.minecraft.client.gui.widget.TextFieldWidget;
|
||||
import net.minecraft.text.Text;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public class PaymentScreen extends Screen {
|
||||
private static final int MAX_RECIPIENT_OPTION_BUTTONS = 10;
|
||||
private static final long RECIPIENT_LOOKUP_DEBOUNCE_MS = 500L;
|
||||
private static final Pattern CARD_ID_PATTERN = Pattern.compile("\\d{5}");
|
||||
private final Screen parent;
|
||||
private final BankUiService bankUiService = BankUiService.instance();
|
||||
private final List<ButtonWidget> recipientCardOptionButtons = new ArrayList<>();
|
||||
private final List<String> recipientCardOptions = new ArrayList<>();
|
||||
private final String initialRecipient;
|
||||
private final UiNotifications notifications = UiNotifications.instance();
|
||||
private TextFieldWidget amountField;
|
||||
private TextFieldWidget recipientField;
|
||||
private TextFieldWidget commentField;
|
||||
private ButtonWidget senderLeftButton;
|
||||
private ButtonWidget senderCardLabelButton;
|
||||
private ButtonWidget senderRightButton;
|
||||
private ButtonWidget recipientCardDropdownButton;
|
||||
private boolean recipientDropdownExpanded;
|
||||
private String selectedRecipientCard = "";
|
||||
private String lastRecipientLookup = "";
|
||||
private String pendingRecipientLookup = "";
|
||||
private long pendingRecipientLookupAt;
|
||||
private Text senderCardText = Text.literal("Карта отправителя: не выбрана");
|
||||
private ButtonWidget transferButton;
|
||||
private ButtonWidget backButton;
|
||||
|
||||
public PaymentScreen(Screen parent) {
|
||||
this(parent, "");
|
||||
}
|
||||
|
||||
public PaymentScreen(Screen parent, String initialRecipient) {
|
||||
super(Text.literal("Оплата"));
|
||||
this.parent = parent;
|
||||
this.initialRecipient = initialRecipient == null ? "" : initialRecipient.trim();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void init() {
|
||||
int centerX = this.width / 2;
|
||||
int startY = this.height / 2 - 82;
|
||||
int leftX = centerX - 210;
|
||||
int rightX = centerX + 14;
|
||||
int leftWidth = 196;
|
||||
int rightWidth = 196;
|
||||
TextRenderer tr = this.textRenderer;
|
||||
|
||||
amountField = new TextFieldWidget(tr, leftX, startY, leftWidth, 20, Text.literal("Сумма"));
|
||||
amountField.setMaxLength(16);
|
||||
amountField.setPlaceholder(Text.literal("Сумма перевода"));
|
||||
this.addDrawableChild(amountField);
|
||||
|
||||
recipientField = new TextFieldWidget(tr, leftX, startY + 28, leftWidth, 20, Text.literal("Получатель"));
|
||||
recipientField.setMaxLength(32);
|
||||
recipientField.setPlaceholder(Text.literal("Ник или 5 цифр карты"));
|
||||
recipientField.setChangedListener(value -> {
|
||||
String input = value.trim();
|
||||
if (isNickname(input)) {
|
||||
scheduleRecipientLookup(input);
|
||||
} else {
|
||||
lastRecipientLookup = "";
|
||||
pendingRecipientLookup = "";
|
||||
recipientCardOptions.clear();
|
||||
selectedRecipientCard = "";
|
||||
}
|
||||
updateRecipientDropdownVisibility();
|
||||
updateSenderCardText();
|
||||
});
|
||||
this.addDrawableChild(recipientField);
|
||||
|
||||
commentField = new TextFieldWidget(tr, leftX, startY + 56, leftWidth, 20, Text.literal("Комментарий"));
|
||||
commentField.setMaxLength(64);
|
||||
commentField.setPlaceholder(Text.literal("Комментарий (необязательно)"));
|
||||
this.addDrawableChild(commentField);
|
||||
|
||||
recipientCardDropdownButton = this.addDrawableChild(ButtonWidget.builder(Text.literal("Карта игрока: -"), button -> {
|
||||
recipientDropdownExpanded = !recipientDropdownExpanded;
|
||||
updateRecipientDropdownVisibility();
|
||||
}).dimensions(leftX, startY + 84, leftWidth, 20).build());
|
||||
|
||||
recipientCardOptionButtons.clear();
|
||||
for (int i = 0; i < MAX_RECIPIENT_OPTION_BUTTONS; i++) {
|
||||
final int index = i;
|
||||
ButtonWidget optionButton = this.addDrawableChild(ButtonWidget.builder(Text.literal("-"), button -> {
|
||||
if (index < recipientCardOptions.size()) {
|
||||
selectedRecipientCard = recipientCardOptions.get(index);
|
||||
recipientDropdownExpanded = false;
|
||||
updateRecipientDropdownVisibility();
|
||||
updateSenderCardText();
|
||||
}
|
||||
}).dimensions(leftX, startY + 108 + i * 22, leftWidth, 20).build());
|
||||
recipientCardOptionButtons.add(optionButton);
|
||||
}
|
||||
|
||||
if (!initialRecipient.isEmpty()) {
|
||||
recipientField.setText(initialRecipient);
|
||||
}
|
||||
|
||||
int arrowWidth = 36;
|
||||
int middleWidth = rightWidth - arrowWidth * 2 - 8;
|
||||
senderLeftButton = this.addDrawableChild(ButtonWidget.builder(Text.literal("<"), button -> {
|
||||
bankUiService.cycleSelectedCard(-1);
|
||||
updateSenderCardSelector();
|
||||
updateSenderCardText();
|
||||
}).dimensions(rightX, startY, arrowWidth, 20).build());
|
||||
|
||||
senderCardLabelButton = this.addDrawableChild(ButtonWidget.builder(Text.literal("00000: 0 АР"), button -> {
|
||||
}).dimensions(rightX + arrowWidth + 4, startY, middleWidth, 20).build());
|
||||
senderCardLabelButton.active = false;
|
||||
|
||||
senderRightButton = this.addDrawableChild(ButtonWidget.builder(Text.literal(">"), button -> {
|
||||
bankUiService.cycleSelectedCard(1);
|
||||
updateSenderCardSelector();
|
||||
updateSenderCardText();
|
||||
}).dimensions(rightX + arrowWidth + 4 + middleWidth + 4, startY, arrowWidth, 20).build());
|
||||
|
||||
transferButton = this.addDrawableChild(ButtonWidget.builder(Text.literal("Перевести"), button -> submit())
|
||||
.dimensions(rightX, startY + 28, rightWidth, 20)
|
||||
.build());
|
||||
|
||||
backButton = this.addDrawableChild(ButtonWidget.builder(Text.literal("Назад"), button -> this.close())
|
||||
.dimensions(rightX, startY + 56, rightWidth, 20)
|
||||
.build());
|
||||
|
||||
updateSenderCardSelector();
|
||||
updateRecipientDropdownVisibility();
|
||||
updateSenderCardText();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
if (this.client != null) {
|
||||
this.client.setScreen(parent);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void tick() {
|
||||
super.tick();
|
||||
if (pendingRecipientLookup.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
long elapsed = System.currentTimeMillis() - pendingRecipientLookupAt;
|
||||
if (elapsed < RECIPIENT_LOOKUP_DEBOUNCE_MS) {
|
||||
return;
|
||||
}
|
||||
|
||||
String lookupUsername = pendingRecipientLookup;
|
||||
pendingRecipientLookup = "";
|
||||
requestRecipientCards(lookupUsername);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void render(DrawContext context, int mouseX, int mouseY, float delta) {
|
||||
super.render(context, mouseX, mouseY, delta);
|
||||
|
||||
int centerX = this.width / 2;
|
||||
int startY = this.height / 2 - 82;
|
||||
int leftX = centerX - 210;
|
||||
int rightX = centerX + 14;
|
||||
|
||||
context.drawCenteredTextWithShadow(this.textRenderer, this.title, centerX, 20, 0xFFFFFF);
|
||||
context.drawTextWithShadow(this.textRenderer, Text.literal("Левая колонка: ввод"), leftX, startY - 18, 0xBFBFBF);
|
||||
context.drawTextWithShadow(this.textRenderer, Text.literal("Правая колонка: действия"), rightX, startY - 18, 0xBFBFBF);
|
||||
context.drawTextWithShadow(this.textRenderer, Text.literal("Сумма:"), leftX, startY - 10, 0xCCCCCC);
|
||||
context.drawTextWithShadow(this.textRenderer, Text.literal("Получатель:"), leftX, startY + 18, 0xCCCCCC);
|
||||
context.drawTextWithShadow(this.textRenderer, Text.literal("Комментарий:"), leftX, startY + 46, 0xCCCCCC);
|
||||
|
||||
if (recipientCardDropdownButton.visible) {
|
||||
context.drawTextWithShadow(this.textRenderer, Text.literal("Карта игрока"), leftX, startY + 74, 0xD7B2FF);
|
||||
}
|
||||
|
||||
context.drawCenteredTextWithShadow(this.textRenderer, senderCardText, centerX, this.height - 20, 0xA9E5A9);
|
||||
notifications.render(context, this.textRenderer, this.width, this.height);
|
||||
}
|
||||
|
||||
private void submit() {
|
||||
CardViewModel selectedCard = bankUiService.getSelectedCard();
|
||||
if (selectedCard == null) {
|
||||
notifications.show(Text.literal("Нет выбранной карты отправителя"));
|
||||
return;
|
||||
}
|
||||
|
||||
long amount;
|
||||
try {
|
||||
amount = Long.parseLong(amountField.getText().trim());
|
||||
if (amount <= 0) {
|
||||
notifications.show(Text.literal("Сумма должна быть больше 0"));
|
||||
return;
|
||||
}
|
||||
} catch (NumberFormatException exception) {
|
||||
notifications.show(Text.literal("Некорректная сумма"));
|
||||
return;
|
||||
}
|
||||
|
||||
String recipientInput = recipientField.getText().trim();
|
||||
if (!isValidRecipient(recipientInput)) {
|
||||
notifications.show(Text.literal("Укажи ник или 5 цифр карты"));
|
||||
return;
|
||||
}
|
||||
|
||||
String receiver = recipientInput;
|
||||
if (isNickname(recipientInput)) {
|
||||
if (selectedRecipientCard.isEmpty()) {
|
||||
notifications.show(Text.literal("Выбери карту получателя из списка"));
|
||||
return;
|
||||
}
|
||||
receiver = selectedRecipientCard;
|
||||
}
|
||||
|
||||
PaymentDraft draft = new PaymentDraft(
|
||||
selectedCard.id(),
|
||||
receiver,
|
||||
amount,
|
||||
commentField.getText().trim()
|
||||
);
|
||||
|
||||
boolean accepted = bankUiService.submitPayment(draft);
|
||||
String serviceMessage = bankUiService.getLastMessage();
|
||||
if (!serviceMessage.isBlank()) {
|
||||
|
||||
notifications.showMessage(serviceMessage);
|
||||
} else {
|
||||
notifications.show(accepted
|
||||
? Text.literal("Перевод выполнен")
|
||||
: Text.literal("Перевод отклонен"));
|
||||
}
|
||||
updateSenderCardSelector();
|
||||
updateSenderCardText();
|
||||
}
|
||||
|
||||
private boolean isValidRecipient(String recipient) {
|
||||
if (recipient.matches("\\d{5}")) {
|
||||
return true;
|
||||
}
|
||||
return recipient.matches("[A-Za-z0-9_]{3,16}");
|
||||
}
|
||||
|
||||
private void updateSenderCardText() {
|
||||
CardViewModel selectedCard = bankUiService.getSelectedCard();
|
||||
if (selectedCard == null) {
|
||||
senderCardText = Text.literal("Карта отправителя: не выбрана");
|
||||
return;
|
||||
}
|
||||
|
||||
String recipient = recipientField == null ? "" : recipientField.getText().trim();
|
||||
String suffix;
|
||||
if (isNickname(recipient)) {
|
||||
String recipientCard = selectedRecipientCard.isEmpty() ? "карта не выбрана" : selectedRecipientCard;
|
||||
suffix = " (ник: выбранная карта -> " + recipientCard + ")";
|
||||
} else {
|
||||
suffix = " (режим: перевод по номеру карты)";
|
||||
}
|
||||
|
||||
senderCardText = Text.literal(
|
||||
"Карта отправителя: " + selectedCard.title() + " | Баланс: " + selectedCard.balance() + suffix
|
||||
);
|
||||
}
|
||||
|
||||
private void updateSenderCardSelector() {
|
||||
CardViewModel selectedCard = bankUiService.getSelectedCard();
|
||||
if (selectedCard == null) {
|
||||
senderCardLabelButton.setMessage(Text.literal("\"00000\": \"0\" АР"));
|
||||
senderLeftButton.active = false;
|
||||
senderRightButton.active = false;
|
||||
return;
|
||||
}
|
||||
|
||||
senderLeftButton.active = true;
|
||||
senderRightButton.active = true;
|
||||
String cardId = extractCardId(selectedCard.title());
|
||||
String balance = Long.toString(selectedCard.balance());
|
||||
senderCardLabelButton.setMessage(Text.literal("\"" + cardId + "\": \"" + balance + "\" АР"));
|
||||
senderCardLabelButton.active = false;
|
||||
}
|
||||
|
||||
private void updateRecipientDropdownVisibility() {
|
||||
if (recipientCardDropdownButton == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
boolean nicknameMode = isNickname(recipientField == null ? "" : recipientField.getText().trim());
|
||||
|
||||
boolean hasRecipientCards = !recipientCardOptions.isEmpty();
|
||||
recipientCardDropdownButton.visible = nicknameMode;
|
||||
recipientCardDropdownButton.active = nicknameMode && hasRecipientCards;
|
||||
|
||||
if (!nicknameMode) {
|
||||
recipientDropdownExpanded = false;
|
||||
selectedRecipientCard = "";
|
||||
} else if (hasRecipientCards && selectedRecipientCard.isEmpty()) {
|
||||
selectedRecipientCard = recipientCardOptions.get(0);
|
||||
}
|
||||
|
||||
String dropdownText;
|
||||
if (!nicknameMode) {
|
||||
dropdownText = "Карта игрока: -";
|
||||
} else if (!hasRecipientCards) {
|
||||
dropdownText = "Карта игрока: нет карт";
|
||||
} else {
|
||||
dropdownText = "Карта игрока: " + selectedRecipientCard;
|
||||
}
|
||||
recipientCardDropdownButton.setMessage(Text.literal(dropdownText));
|
||||
|
||||
for (int i = 0; i < recipientCardOptionButtons.size(); i++) {
|
||||
ButtonWidget optionButton = recipientCardOptionButtons.get(i);
|
||||
boolean showOption = nicknameMode && recipientDropdownExpanded && i < recipientCardOptions.size();
|
||||
optionButton.visible = showOption;
|
||||
optionButton.active = showOption;
|
||||
if (i < recipientCardOptions.size()) {
|
||||
optionButton.setMessage(Text.literal(recipientCardOptions.get(i)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isNickname(String value) {
|
||||
return value.matches("[A-Za-z0-9_]{3,16}") && !value.matches("\\d+");
|
||||
}
|
||||
|
||||
private String extractCardId(String source) {
|
||||
Matcher matcher = CARD_ID_PATTERN.matcher(source);
|
||||
if (matcher.find()) {
|
||||
return matcher.group();
|
||||
}
|
||||
return "00000";
|
||||
}
|
||||
|
||||
private void requestRecipientCards(String username) {
|
||||
if (username.equals(lastRecipientLookup)) {
|
||||
return;
|
||||
}
|
||||
lastRecipientLookup = username;
|
||||
|
||||
CompletableFuture
|
||||
.supplyAsync(() -> bankUiService.loadRecipientCards(username))
|
||||
.thenAccept(cards -> {
|
||||
if (this.client == null) {
|
||||
return;
|
||||
}
|
||||
this.client.execute(() -> {
|
||||
if (recipientField == null || !username.equals(recipientField.getText().trim())) {
|
||||
return;
|
||||
}
|
||||
|
||||
recipientCardOptions.clear();
|
||||
recipientCardOptions.addAll(cards);
|
||||
selectedRecipientCard = recipientCardOptions.isEmpty() ? "" : recipientCardOptions.get(0);
|
||||
recipientDropdownExpanded = false;
|
||||
|
||||
String serviceMessage = bankUiService.getLastMessage();
|
||||
if (!serviceMessage.isBlank()) {
|
||||
notifications.showMessage(serviceMessage);
|
||||
}
|
||||
|
||||
updateRecipientDropdownVisibility();
|
||||
updateSenderCardText();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private void scheduleRecipientLookup(String username) {
|
||||
pendingRecipientLookup = username;
|
||||
pendingRecipientLookupAt = System.currentTimeMillis();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
package git.yawaflua.tech.spmega.client.ui;
|
||||
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParser;
|
||||
import com.google.gson.Strictness;
|
||||
import com.google.gson.stream.JsonReader;
|
||||
import net.minecraft.client.font.TextRenderer;
|
||||
import net.minecraft.client.gui.DrawContext;
|
||||
import net.minecraft.text.Text;
|
||||
|
||||
import java.awt.*;
|
||||
|
||||
public final class UiNotifications {
|
||||
private static final long DEFAULT_DURATION_MS = 3500L;
|
||||
private static final UiNotifications INSTANCE = new UiNotifications();
|
||||
|
||||
private Text currentText = Text.empty();
|
||||
private long visibleUntilMs;
|
||||
|
||||
private UiNotifications() {
|
||||
}
|
||||
|
||||
public static UiNotifications instance() {
|
||||
return INSTANCE;
|
||||
}
|
||||
|
||||
public static String extractMessage(String raw) {
|
||||
if (raw == null || raw.isBlank()) {
|
||||
return "";
|
||||
}
|
||||
|
||||
try {
|
||||
System.out.println(raw);
|
||||
var reader = new JsonReader(new java.io.StringReader(raw));
|
||||
reader.setStrictness(Strictness.LENIENT);
|
||||
|
||||
JsonElement parsed = JsonParser.parseReader(reader);
|
||||
JsonObject object = parsed.isJsonArray() ? parsed.getAsJsonArray().get(0).getAsJsonObject() : parsed.getAsJsonObject();
|
||||
if (object.has("message") && !object.get("message").isJsonNull()) {
|
||||
return object.get("message").getAsString();
|
||||
}
|
||||
|
||||
} catch (Exception ignored) {
|
||||
System.out.println(ignored.getMessage());
|
||||
// fallback to raw text
|
||||
}
|
||||
|
||||
return raw;
|
||||
}
|
||||
|
||||
public synchronized void show(Text text) {
|
||||
if (text == null || text.getString().isBlank()) {
|
||||
return;
|
||||
}
|
||||
currentText = text;
|
||||
visibleUntilMs = System.currentTimeMillis() + DEFAULT_DURATION_MS;
|
||||
}
|
||||
|
||||
public synchronized void showMessage(String message) {
|
||||
if (message == null || message.isBlank()) {
|
||||
return;
|
||||
}
|
||||
show(Text.literal(extractMessage(message)));
|
||||
}
|
||||
|
||||
public synchronized void render(DrawContext context, TextRenderer textRenderer, int width, int height) {
|
||||
if (currentText == null || currentText.getString().isBlank()) {
|
||||
return;
|
||||
}
|
||||
if (System.currentTimeMillis() > visibleUntilMs) {
|
||||
return;
|
||||
}
|
||||
|
||||
String message = currentText.getString();
|
||||
int padding = 6;
|
||||
int textWidth = textRenderer.getWidth(message);
|
||||
int boxWidth = textWidth + padding * 2;
|
||||
int boxHeight = textRenderer.fontHeight + padding * 2;
|
||||
int x = width - boxWidth - 10;
|
||||
int y = height - boxHeight - 10;
|
||||
|
||||
context.fill(x, y, x + boxWidth, y + boxHeight, Color.GRAY.getRGB());
|
||||
context.drawTextWithShadow(textRenderer, currentText, x + padding, y + padding, Color.WHITE.getRGB());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
package git.yawaflua.tech.spmega.client.ui;
|
||||
|
||||
import net.minecraft.client.MinecraftClient;
|
||||
import net.minecraft.client.gui.screen.Screen;
|
||||
|
||||
public final class UiOpeners {
|
||||
private UiOpeners() {
|
||||
}
|
||||
|
||||
public static void openMainMenu(MinecraftClient client) {
|
||||
if (client == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
Screen current = client.currentScreen;
|
||||
client.setScreen(new MainBankScreen(current));
|
||||
}
|
||||
|
||||
public static void openPaymentMenu(MinecraftClient client, String recipient) {
|
||||
if (client == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
Screen current = client.currentScreen;
|
||||
client.setScreen(new PaymentScreen(current, recipient));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,173 @@
|
||||
package git.yawaflua.tech.spmega.client.ui.service;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.sql.*;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public final class BankDatabase {
|
||||
private final String jdbcUrl;
|
||||
|
||||
public BankDatabase(Path databasePath) {
|
||||
this.jdbcUrl = "jdbc:sqlite:" + databasePath.toAbsolutePath();
|
||||
}
|
||||
|
||||
public void initialize() {
|
||||
try (Connection connection = open()) {
|
||||
try (Statement statement = connection.createStatement()) {
|
||||
statement.executeUpdate("""
|
||||
CREATE TABLE IF NOT EXISTS cards (
|
||||
card_id TEXT PRIMARY KEY,
|
||||
card_token TEXT NOT NULL,
|
||||
card_name TEXT,
|
||||
card_number TEXT,
|
||||
balance INTEGER NOT NULL DEFAULT 0,
|
||||
owner_uuid TEXT,
|
||||
updated_at TEXT DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
""");
|
||||
statement.executeUpdate("""
|
||||
CREATE TABLE IF NOT EXISTS transfer_history (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
sender_card_id TEXT NOT NULL,
|
||||
receiver TEXT NOT NULL,
|
||||
amount INTEGER NOT NULL,
|
||||
comment TEXT,
|
||||
balance_after INTEGER,
|
||||
status TEXT NOT NULL,
|
||||
created_at TEXT DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
""");
|
||||
}
|
||||
} catch (SQLException exception) {
|
||||
throw new RuntimeException("Failed to initialize sqlite database", exception);
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized void upsertCardCredentials(String cardId, String cardToken) {
|
||||
String sql = """
|
||||
INSERT INTO cards(card_id, card_token, updated_at)
|
||||
VALUES(?, ?, CURRENT_TIMESTAMP)
|
||||
ON CONFLICT(card_id) DO UPDATE SET
|
||||
card_token = excluded.card_token,
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
""";
|
||||
try (Connection connection = open(); PreparedStatement statement = connection.prepareStatement(sql)) {
|
||||
statement.setString(1, cardId);
|
||||
statement.setString(2, cardToken);
|
||||
statement.executeUpdate();
|
||||
} catch (SQLException exception) {
|
||||
throw new RuntimeException("Failed to upsert card credentials", exception);
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized void updateCardMeta(String cardId, String cardName, String cardNumber, long balance, String ownerUuid) {
|
||||
String sql = """
|
||||
UPDATE cards
|
||||
SET card_name = ?, card_number = ?, balance = ?, owner_uuid = ?, updated_at = CURRENT_TIMESTAMP
|
||||
WHERE card_id = ?
|
||||
""";
|
||||
try (Connection connection = open(); PreparedStatement statement = connection.prepareStatement(sql)) {
|
||||
statement.setString(1, cardName);
|
||||
statement.setString(2, cardNumber);
|
||||
statement.setLong(3, balance);
|
||||
statement.setString(4, ownerUuid);
|
||||
statement.setString(5, cardId);
|
||||
statement.executeUpdate();
|
||||
} catch (SQLException exception) {
|
||||
throw new RuntimeException("Failed to update card meta", exception);
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized void updateCardBalance(String cardId, long balance) {
|
||||
String sql = "UPDATE cards SET balance = ?, updated_at = CURRENT_TIMESTAMP WHERE card_id = ?";
|
||||
try (Connection connection = open(); PreparedStatement statement = connection.prepareStatement(sql)) {
|
||||
statement.setLong(1, balance);
|
||||
statement.setString(2, cardId);
|
||||
statement.executeUpdate();
|
||||
} catch (SQLException exception) {
|
||||
throw new RuntimeException("Failed to update card balance", exception);
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized void deleteCard(String cardId) {
|
||||
String sql = "DELETE FROM cards WHERE card_id = ?";
|
||||
try (Connection connection = open(); PreparedStatement statement = connection.prepareStatement(sql)) {
|
||||
statement.setString(1, cardId);
|
||||
statement.executeUpdate();
|
||||
} catch (SQLException exception) {
|
||||
throw new RuntimeException("Failed to delete card", exception);
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized List<StoredCard> loadCards() {
|
||||
String sql = "SELECT card_id, card_token, card_name, card_number, balance, owner_uuid FROM cards ORDER BY updated_at DESC";
|
||||
List<StoredCard> result = new ArrayList<>();
|
||||
try (Connection connection = open();
|
||||
PreparedStatement statement = connection.prepareStatement(sql);
|
||||
ResultSet rs = statement.executeQuery()) {
|
||||
while (rs.next()) {
|
||||
result.add(new StoredCard(
|
||||
rs.getString("card_id"),
|
||||
rs.getString("card_token"),
|
||||
rs.getString("card_name"),
|
||||
rs.getString("card_number"),
|
||||
rs.getLong("balance"),
|
||||
rs.getString("owner_uuid")
|
||||
));
|
||||
}
|
||||
return result;
|
||||
} catch (SQLException exception) {
|
||||
throw new RuntimeException("Failed to load cards", exception);
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized CardCredentials getCredentials(String cardId) {
|
||||
String sql = "SELECT card_id, card_token FROM cards WHERE card_id = ?";
|
||||
try (Connection connection = open(); PreparedStatement statement = connection.prepareStatement(sql)) {
|
||||
statement.setString(1, cardId);
|
||||
try (ResultSet rs = statement.executeQuery()) {
|
||||
if (rs.next()) {
|
||||
return new CardCredentials(rs.getString("card_id"), rs.getString("card_token"));
|
||||
}
|
||||
}
|
||||
return null;
|
||||
} catch (SQLException exception) {
|
||||
throw new RuntimeException("Failed to get card credentials", exception);
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized void insertTransferHistory(
|
||||
String senderCardId,
|
||||
String receiver,
|
||||
long amount,
|
||||
String comment,
|
||||
Long balanceAfter,
|
||||
String status
|
||||
) {
|
||||
String sql = """
|
||||
INSERT INTO transfer_history(sender_card_id, receiver, amount, comment, balance_after, status)
|
||||
VALUES(?, ?, ?, ?, ?, ?)
|
||||
""";
|
||||
try (Connection connection = open(); PreparedStatement statement = connection.prepareStatement(sql)) {
|
||||
statement.setString(1, senderCardId);
|
||||
statement.setString(2, receiver);
|
||||
statement.setLong(3, amount);
|
||||
statement.setString(4, comment);
|
||||
if (balanceAfter == null) {
|
||||
statement.setNull(5, java.sql.Types.INTEGER);
|
||||
} else {
|
||||
statement.setLong(5, balanceAfter);
|
||||
}
|
||||
statement.setString(6, status);
|
||||
statement.executeUpdate();
|
||||
} catch (SQLException exception) {
|
||||
throw new RuntimeException("Failed to insert transfer history", exception);
|
||||
}
|
||||
}
|
||||
|
||||
private Connection open() throws SQLException {
|
||||
return DriverManager.getConnection(jdbcUrl);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,298 @@
|
||||
package git.yawaflua.tech.spmega.client.ui.service;
|
||||
|
||||
import git.yawaflua.tech.spmega.api.SPWorldsApiClient;
|
||||
import net.fabricmc.loader.api.FabricLoader;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
public final class BankUiService {
|
||||
private static final BankUiService INSTANCE = new BankUiService();
|
||||
|
||||
private final List<CardViewModel> cards = new ArrayList<>();
|
||||
private final BankDatabase database;
|
||||
private final SPWorldsApiClient apiClient;
|
||||
private final String apiDomain = "https://spworlds.ru";
|
||||
|
||||
private int selectedCardIndex;
|
||||
private String lastMessage = "";
|
||||
|
||||
private BankUiService() {
|
||||
this.database = new BankDatabase(FabricLoader.getInstance().getConfigDir().resolve("spmega.db"));
|
||||
this.database.initialize();
|
||||
this.apiClient = new SPWorldsApiClient(apiDomain);
|
||||
|
||||
reloadCardsFromDb();
|
||||
}
|
||||
|
||||
public static BankUiService instance() {
|
||||
return INSTANCE;
|
||||
}
|
||||
|
||||
private static SPWorldsApiClient.CardAuth toApiAuth(CardCredentials credentials) {
|
||||
return new SPWorldsApiClient.CardAuth(credentials.cardId(), credentials.cardToken());
|
||||
}
|
||||
|
||||
private static String trimMessage(String message) {
|
||||
if (message == null) {
|
||||
return "unknown";
|
||||
}
|
||||
return message.length() > 120 ? message.substring(0, 120) : message;
|
||||
}
|
||||
|
||||
private static String normalizeUuid(String rawUuid) {
|
||||
return rawUuid == null ? "" : rawUuid.toLowerCase(Locale.ROOT).replace("-", "");
|
||||
}
|
||||
|
||||
private static String extractLastDigits(String value) {
|
||||
String normalized = value == null ? "" : value.replace("-", "");
|
||||
if (normalized.length() < 5) {
|
||||
return "00000";
|
||||
}
|
||||
return normalized.substring(normalized.length() - 5);
|
||||
}
|
||||
|
||||
public synchronized List<CardViewModel> getCards() {
|
||||
return Collections.unmodifiableList(cards);
|
||||
}
|
||||
|
||||
public synchronized CardViewModel getSelectedCard() {
|
||||
if (cards.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
selectedCardIndex = Math.floorMod(selectedCardIndex, cards.size());
|
||||
return cards.get(selectedCardIndex);
|
||||
}
|
||||
|
||||
public synchronized int getSelectedCardIndex() {
|
||||
if (cards.isEmpty()) {
|
||||
return 0;
|
||||
}
|
||||
selectedCardIndex = Math.floorMod(selectedCardIndex, cards.size());
|
||||
return selectedCardIndex;
|
||||
}
|
||||
|
||||
public synchronized void setSelectedCardIndex(int index) {
|
||||
if (cards.isEmpty()) {
|
||||
selectedCardIndex = 0;
|
||||
return;
|
||||
}
|
||||
selectedCardIndex = Math.floorMod(index, cards.size());
|
||||
}
|
||||
|
||||
public synchronized CardViewModel cycleSelectedCard(int direction) {
|
||||
if (cards.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
selectedCardIndex = Math.floorMod(selectedCardIndex + direction, cards.size());
|
||||
return cards.get(selectedCardIndex);
|
||||
}
|
||||
|
||||
public synchronized String getLastMessage() {
|
||||
return lastMessage;
|
||||
}
|
||||
|
||||
public synchronized void refreshOnServerJoin(String playerUuid) {
|
||||
List<StoredCard> storedCards = database.loadCards();
|
||||
for (StoredCard card : storedCards) {
|
||||
refreshCard(card.cardId(), card.cardToken(), playerUuid, false, false);
|
||||
}
|
||||
reloadCardsFromDb();
|
||||
}
|
||||
|
||||
public synchronized List<String> loadRecipientCards(String username) {
|
||||
CardCredentials credentials = getSelectedCredentials();
|
||||
if (credentials == null || username == null || username.isBlank()) {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
try {
|
||||
List<SPWorldsApiClient.PlayerCard> apiCards = apiClient.getPlayerCards(username, toApiAuth(credentials));
|
||||
List<String> numbers = new ArrayList<>();
|
||||
for (SPWorldsApiClient.PlayerCard apiCard : apiCards) {
|
||||
if (apiCard.number() != null && !apiCard.number().isBlank()) {
|
||||
numbers.add(apiCard.number());
|
||||
}
|
||||
}
|
||||
lastMessage = "";
|
||||
return numbers;
|
||||
} catch (Exception exception) {
|
||||
lastMessage = "Не удалось получить карты игрока: " + exception.getMessage();
|
||||
return List.of();
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized String addCard(String cardIdRaw, String cardTokenRaw, String playerUuid) {
|
||||
String cardId = cardIdRaw == null ? "" : cardIdRaw.trim();
|
||||
String cardToken = cardTokenRaw == null ? "" : cardTokenRaw.trim();
|
||||
|
||||
if (cardId.isEmpty() || cardToken.isEmpty()) {
|
||||
lastMessage = "Укажи cardId и cardToken";
|
||||
return lastMessage;
|
||||
}
|
||||
|
||||
try {
|
||||
UUID.fromString(cardId);
|
||||
} catch (IllegalArgumentException exception) {
|
||||
lastMessage = "cardId должен быть UUID";
|
||||
return lastMessage;
|
||||
}
|
||||
|
||||
database.upsertCardCredentials(cardId, cardToken);
|
||||
boolean refreshed = refreshCard(cardId, cardToken, playerUuid, true, true);
|
||||
if (!refreshed) {
|
||||
database.deleteCard(cardId);
|
||||
}
|
||||
reloadCardsFromDb();
|
||||
|
||||
if (refreshed && lastMessage.isBlank()) {
|
||||
lastMessage = "Карта добавлена";
|
||||
}
|
||||
return lastMessage;
|
||||
}
|
||||
|
||||
public synchronized void removeSelectedCard() {
|
||||
CardViewModel selected = getSelectedCard();
|
||||
if (selected == null) {
|
||||
lastMessage = "Нет карты для удаления";
|
||||
return;
|
||||
}
|
||||
|
||||
database.deleteCard(selected.id());
|
||||
reloadCardsFromDb();
|
||||
if (selectedCardIndex >= cards.size()) {
|
||||
selectedCardIndex = Math.max(0, cards.size() - 1);
|
||||
}
|
||||
lastMessage = "Карта удалена";
|
||||
}
|
||||
|
||||
public synchronized void refreshSelectedCard(String playerUuid) {
|
||||
CardViewModel selected = getSelectedCard();
|
||||
if (selected == null) {
|
||||
lastMessage = "Нет карты для обновления";
|
||||
return;
|
||||
}
|
||||
|
||||
CardCredentials credentials = database.getCredentials(selected.id());
|
||||
if (credentials == null) {
|
||||
lastMessage = "Не найдены креды карты";
|
||||
return;
|
||||
}
|
||||
|
||||
refreshCard(credentials.cardId(), credentials.cardToken(), playerUuid, false, false);
|
||||
reloadCardsFromDb();
|
||||
}
|
||||
|
||||
public synchronized boolean submitPayment(PaymentDraft draft) {
|
||||
CardCredentials credentials = database.getCredentials(draft.senderCardId());
|
||||
if (credentials == null) {
|
||||
lastMessage = "Не найдены креды карты отправителя";
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
SPWorldsApiClient.TransactionResult result = apiClient.createTransaction(
|
||||
toApiAuth(credentials),
|
||||
draft.recipient(),
|
||||
draft.amount(),
|
||||
draft.comment()
|
||||
);
|
||||
|
||||
database.updateCardBalance(credentials.cardId(), result.balance());
|
||||
database.insertTransferHistory(
|
||||
credentials.cardId(),
|
||||
draft.recipient(),
|
||||
draft.amount(),
|
||||
draft.comment(),
|
||||
result.balance(),
|
||||
"SUCCESS"
|
||||
);
|
||||
|
||||
reloadCardsFromDb();
|
||||
lastMessage = "Перевод выполнен";
|
||||
return true;
|
||||
} catch (Exception exception) {
|
||||
database.insertTransferHistory(
|
||||
credentials.cardId(),
|
||||
draft.recipient(),
|
||||
draft.amount(),
|
||||
draft.comment(),
|
||||
null,
|
||||
"FAILED: " + trimMessage(exception.getMessage())
|
||||
);
|
||||
lastMessage = "Ошибка перевода: " + exception.getMessage();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean refreshCard(
|
||||
String cardId,
|
||||
String cardToken,
|
||||
String playerUuid,
|
||||
boolean reportOwnerWarning,
|
||||
boolean requireCardInAccount
|
||||
) {
|
||||
try {
|
||||
SPWorldsApiClient.CardAuth auth = new SPWorldsApiClient.CardAuth(cardId, cardToken);
|
||||
SPWorldsApiClient.AccountMe me = apiClient.getAccountMe(auth);
|
||||
SPWorldsApiClient.CardInfo cardInfo = apiClient.getCardInfo(auth);
|
||||
|
||||
SPWorldsApiClient.AccountCard currentCard = me.cards().stream()
|
||||
.filter(card -> Objects.equals(card.id(), cardId))
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
|
||||
if (requireCardInAccount && currentCard == null) {
|
||||
lastMessage = "Карта не найдена в аккаунте SPWorlds";
|
||||
return false;
|
||||
}
|
||||
|
||||
String cardName = currentCard == null || currentCard.name().isBlank() ? "Карта" : currentCard.name();
|
||||
String cardNumber = currentCard == null || currentCard.number().isBlank() ? extractLastDigits(cardId) : currentCard.number();
|
||||
String ownerUuid = normalizeUuid(me.minecraftUuid());
|
||||
|
||||
database.updateCardMeta(cardId, cardName, cardNumber, cardInfo.balance(), ownerUuid);
|
||||
|
||||
if (reportOwnerWarning && playerUuid != null && !playerUuid.isBlank()) {
|
||||
String normalizedPlayerUuid = normalizeUuid(playerUuid);
|
||||
if (!normalizedPlayerUuid.equals(ownerUuid)) {
|
||||
lastMessage = "Вы не владелец карты. Часть функций может быть ограничена.";
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
lastMessage = "";
|
||||
return true;
|
||||
} catch (Exception exception) {
|
||||
lastMessage = "Не удалось обновить карту: " + exception.getMessage();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private void reloadCardsFromDb() {
|
||||
List<StoredCard> storedCards = database.loadCards();
|
||||
cards.clear();
|
||||
|
||||
for (StoredCard stored : storedCards) {
|
||||
String cardNumber = stored.cardNumber() == null || stored.cardNumber().isBlank()
|
||||
? extractLastDigits(stored.cardId())
|
||||
: stored.cardNumber();
|
||||
String cardName = stored.cardName() == null || stored.cardName().isBlank() ? "Карта" : stored.cardName();
|
||||
String title = cardNumber + ": " + cardName;
|
||||
cards.add(new CardViewModel(stored.cardId(), title, stored.balance()));
|
||||
}
|
||||
|
||||
if (cards.isEmpty()) {
|
||||
selectedCardIndex = 0;
|
||||
} else {
|
||||
selectedCardIndex = Math.floorMod(selectedCardIndex, cards.size());
|
||||
}
|
||||
}
|
||||
|
||||
private CardCredentials getSelectedCredentials() {
|
||||
CardViewModel selected = getSelectedCard();
|
||||
if (selected == null) {
|
||||
return null;
|
||||
}
|
||||
return database.getCredentials(selected.id());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package git.yawaflua.tech.spmega.client.ui.service;
|
||||
|
||||
public record CardCredentials(String cardId, String cardToken) {
|
||||
}
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
package git.yawaflua.tech.spmega.client.ui.service;
|
||||
|
||||
public record CardViewModel(String id, String title, long balance) {
|
||||
}
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
package git.yawaflua.tech.spmega.client.ui.service;
|
||||
|
||||
public record PaymentDraft(String senderCardId, String recipient, long amount, String comment) {
|
||||
}
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
package git.yawaflua.tech.spmega.client.ui.service;
|
||||
|
||||
public record StoredCard(String cardId, String cardToken, String cardName, String cardNumber, long balance,
|
||||
String ownerUuid) {
|
||||
}
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
package git.yawaflua.tech.spmega.mixin.client;
|
||||
|
||||
import git.yawaflua.tech.spmega.client.ui.UiOpeners;
|
||||
import net.minecraft.client.gui.screen.GameMenuScreen;
|
||||
import net.minecraft.client.gui.screen.Screen;
|
||||
import net.minecraft.client.gui.widget.ButtonWidget;
|
||||
import net.minecraft.text.Text;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
import org.spongepowered.asm.mixin.injection.Inject;
|
||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
|
||||
|
||||
@Mixin(GameMenuScreen.class)
|
||||
public abstract class GameMenuScreenMixin extends Screen {
|
||||
protected GameMenuScreenMixin(Text title) {
|
||||
super(title);
|
||||
}
|
||||
|
||||
@Inject(method = "init", at = @At("TAIL"))
|
||||
private void spmega$addOpenMenuButton(CallbackInfo ci) {
|
||||
int buttonWidth = 96;
|
||||
int x = this.width / 2 + 106;
|
||||
int y = this.height / 4 + 24 - 16;
|
||||
this.addDrawableChild(ButtonWidget.builder(Text.literal("SPMega"), button -> {
|
||||
UiOpeners.openMainMenu(this.client);
|
||||
}).dimensions(x, y, buttonWidth, 20).build());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"category.spmega": "SPMega",
|
||||
"key.spmega.open_menu": "Open SPMega Menu"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"required": true,
|
||||
"minVersion": "0.8",
|
||||
"package": "git.yawaflua.tech.spmega.mixin.client",
|
||||
"compatibilityLevel": "JAVA_21",
|
||||
"client": [
|
||||
"GameMenuScreenMixin"
|
||||
],
|
||||
"injectors": {
|
||||
"defaultRequire": 1
|
||||
},
|
||||
"overwrites": {
|
||||
"requireAnnotations": true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
package git.yawaflua.tech.spmega;
|
||||
|
||||
import net.fabricmc.loader.api.FabricLoader;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Properties;
|
||||
import java.util.UUID;
|
||||
|
||||
public final class ConfigManager {
|
||||
private static final String FILE_NAME = "spmega.properties";
|
||||
|
||||
private ConfigManager() {
|
||||
}
|
||||
|
||||
public static ModConfig loadOrCreate() {
|
||||
Path configPath = FabricLoader.getInstance().getConfigDir().resolve(FILE_NAME);
|
||||
Properties properties = new Properties();
|
||||
ModConfig defaults = ModConfig.createDefault();
|
||||
boolean shouldSave = false;
|
||||
|
||||
if (Files.exists(configPath)) {
|
||||
try (InputStream inputStream = Files.newInputStream(configPath)) {
|
||||
properties.load(inputStream);
|
||||
} catch (IOException exception) {
|
||||
shouldSave = true;
|
||||
}
|
||||
} else {
|
||||
shouldSave = true;
|
||||
}
|
||||
|
||||
String apiDomain = readString(properties, "api.domain", defaults.apiDomain());
|
||||
if (!apiDomain.equals(properties.getProperty("api.domain"))) {
|
||||
shouldSave = true;
|
||||
}
|
||||
|
||||
String apiToken = readString(properties, "api.token", defaults.apiToken());
|
||||
if (!apiToken.equals(properties.getProperty("api.token"))) {
|
||||
shouldSave = true;
|
||||
}
|
||||
|
||||
boolean signQuickPayEnabled = readBoolean(properties, "sign.quickPay.enabled", defaults.signQuickPayEnabled());
|
||||
String rawQuickPay = properties.getProperty("sign.quickPay.enabled");
|
||||
if (rawQuickPay == null || !Boolean.toString(signQuickPayEnabled).equalsIgnoreCase(rawQuickPay.trim())) {
|
||||
shouldSave = true;
|
||||
}
|
||||
|
||||
ModConfig config = new ModConfig(apiDomain, apiToken, signQuickPayEnabled);
|
||||
|
||||
if (shouldSave) {
|
||||
save(configPath, config);
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
private static String readString(Properties properties, String key, String fallback) {
|
||||
String value = properties.getProperty(key);
|
||||
if (value == null) {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
String trimmed = value.trim();
|
||||
return trimmed.isEmpty() ? fallback : trimmed;
|
||||
}
|
||||
|
||||
private static UUID readUuid(Properties properties, String key, UUID fallback) {
|
||||
String value = properties.getProperty(key);
|
||||
if (value == null || value.trim().isEmpty()) {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
try {
|
||||
return UUID.fromString(value.trim());
|
||||
} catch (IllegalArgumentException exception) {
|
||||
// Self-heal broken UUID values by falling back to a valid default.
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean readBoolean(Properties properties, String key, boolean fallback) {
|
||||
String value = properties.getProperty(key);
|
||||
if (value == null) {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
String normalized = value.trim().toLowerCase();
|
||||
if ("true".equals(normalized)) {
|
||||
return true;
|
||||
}
|
||||
if ("false".equals(normalized)) {
|
||||
return false;
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
|
||||
private static void save(Path configPath, ModConfig config) {
|
||||
Properties properties = new Properties();
|
||||
properties.setProperty("api.domain", config.apiDomain());
|
||||
properties.setProperty("api.token", config.apiToken());
|
||||
properties.setProperty("sign.quickPay.enabled", Boolean.toString(config.signQuickPayEnabled()));
|
||||
|
||||
try {
|
||||
Files.createDirectories(configPath.getParent());
|
||||
try (OutputStream outputStream = Files.newOutputStream(configPath)) {
|
||||
properties.store(outputStream, "SPMega config");
|
||||
}
|
||||
} catch (IOException exception) {
|
||||
throw new RuntimeException("Failed to save config: " + configPath, exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
package git.yawaflua.tech.spmega;
|
||||
|
||||
public record ModConfig(String apiDomain, String apiToken, boolean signQuickPayEnabled) {
|
||||
public static final String DEFAULT_API_DOMAIN = "https://spworlds.ru";
|
||||
public static final String DEFAULT_API_TOKEN = "ulBKE9MWEtIGiPAhXV69I28W9BRiSrV3";
|
||||
public static final boolean DEFAULT_SIGN_QUICK_PAY_ENABLED = true;
|
||||
|
||||
public static ModConfig createDefault() {
|
||||
return new ModConfig(
|
||||
DEFAULT_API_DOMAIN,
|
||||
DEFAULT_API_TOKEN,
|
||||
DEFAULT_SIGN_QUICK_PAY_ENABLED
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
package git.yawaflua.tech.spmega;
|
||||
|
||||
import net.fabricmc.api.ModInitializer;
|
||||
|
||||
public class SPMega implements ModInitializer {
|
||||
private static ModConfig config;
|
||||
|
||||
public static ModConfig getConfig() {
|
||||
return config;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onInitialize() {
|
||||
config = ConfigManager.loadOrCreate();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
package git.yawaflua.tech.spmega.api;
|
||||
|
||||
import com.google.gson.*;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Base64;
|
||||
import java.util.List;
|
||||
|
||||
public final class SPWorldsApiClient {
|
||||
private final HttpClient httpClient;
|
||||
private final Gson gson;
|
||||
private final String baseUrl;
|
||||
|
||||
public SPWorldsApiClient(String baseUrl) {
|
||||
this.httpClient = HttpClient.newHttpClient();
|
||||
this.gson = new Gson();
|
||||
this.baseUrl = normalizeBaseUrl(baseUrl);
|
||||
}
|
||||
|
||||
private static String encodeAuth(CardAuth auth) {
|
||||
String raw = auth.cardId() + ":" + auth.cardToken();
|
||||
return Base64.getEncoder().encodeToString(raw.getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
private static String normalizeBaseUrl(String rawBaseUrl) {
|
||||
String fallback = "https://spworlds.ru";
|
||||
if (rawBaseUrl == null || rawBaseUrl.trim().isEmpty()) {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
String value = rawBaseUrl.trim();
|
||||
if (value.endsWith("/")) {
|
||||
return value.substring(0, value.length() - 1);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
private static String getString(JsonObject json, String key) {
|
||||
if (!json.has(key) || json.get(key).isJsonNull()) {
|
||||
return "";
|
||||
}
|
||||
return json.get(key).getAsString();
|
||||
}
|
||||
|
||||
public CardInfo getCardInfo(CardAuth auth) throws IOException, InterruptedException {
|
||||
HttpRequest request = requestBuilder("/api/public/card", auth).GET().build();
|
||||
String body = send(request);
|
||||
JsonObject json = JsonParser.parseString(body).getAsJsonObject();
|
||||
long balance = json.has("balance") ? json.get("balance").getAsLong() : 0L;
|
||||
String webhook = json.has("webhook") && !json.get("webhook").isJsonNull()
|
||||
? json.get("webhook").getAsString()
|
||||
: "";
|
||||
return new CardInfo(balance, webhook);
|
||||
}
|
||||
|
||||
public List<PlayerCard> getPlayerCards(String username, CardAuth auth) throws IOException, InterruptedException {
|
||||
HttpRequest request = requestBuilder("/api/public/accounts/" + username + "/cards", auth).GET().build();
|
||||
String body = send(request);
|
||||
JsonArray json = JsonParser.parseString(body).getAsJsonArray();
|
||||
|
||||
List<PlayerCard> cards = new ArrayList<>();
|
||||
for (JsonElement element : json) {
|
||||
JsonObject card = element.getAsJsonObject();
|
||||
String name = card.has("name") ? card.get("name").getAsString() : "";
|
||||
String number = card.has("number") ? card.get("number").getAsString() : "";
|
||||
cards.add(new PlayerCard(name, number));
|
||||
}
|
||||
return cards;
|
||||
}
|
||||
|
||||
public AccountMe getAccountMe(CardAuth auth) throws IOException, InterruptedException {
|
||||
HttpRequest request = requestBuilder("/api/public/accounts/me", auth).GET().build();
|
||||
String body = send(request);
|
||||
JsonObject json = JsonParser.parseString(body).getAsJsonObject();
|
||||
|
||||
String id = json.has("id") ? json.get("id").getAsString() : "";
|
||||
String username = json.has("username") ? json.get("username").getAsString() : "";
|
||||
String minecraftUuid = json.has("minecraftUUID") ? json.get("minecraftUUID").getAsString() : "";
|
||||
|
||||
List<AccountCard> cards = new ArrayList<>();
|
||||
if (json.has("cards") && json.get("cards").isJsonArray()) {
|
||||
for (JsonElement element : json.getAsJsonArray("cards")) {
|
||||
JsonObject card = element.getAsJsonObject();
|
||||
cards.add(new AccountCard(
|
||||
getString(card, "id"),
|
||||
getString(card, "name"),
|
||||
getString(card, "number"),
|
||||
card.has("color") && !card.get("color").isJsonNull() ? card.get("color").getAsInt() : 0
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
return new AccountMe(id, username, minecraftUuid, cards);
|
||||
}
|
||||
|
||||
public TransactionResult createTransaction(CardAuth auth, String receiver, long amount, String comment)
|
||||
throws IOException, InterruptedException {
|
||||
JsonObject payload = new JsonObject();
|
||||
payload.addProperty("receiver", receiver);
|
||||
payload.addProperty("amount", amount);
|
||||
payload.addProperty("comment", comment.isEmpty() ? "Перевод через SPMega" : comment);
|
||||
|
||||
HttpRequest request = requestBuilder("/api/public/transactions", auth)
|
||||
.POST(HttpRequest.BodyPublishers.ofString(gson.toJson(payload)))
|
||||
.build();
|
||||
|
||||
String body = send(request);
|
||||
JsonObject json = JsonParser.parseString(body).getAsJsonObject();
|
||||
long balance = json.has("balance") ? json.get("balance").getAsLong() : 0L;
|
||||
return new TransactionResult(balance);
|
||||
}
|
||||
|
||||
private HttpRequest.Builder requestBuilder(String path, CardAuth auth) {
|
||||
return HttpRequest.newBuilder(URI.create(baseUrl + path))
|
||||
.header("Authorization", "Bearer " + encodeAuth(auth))
|
||||
.header("Content-Type", "application/json")
|
||||
.header("Accept", "application/json");
|
||||
}
|
||||
|
||||
private String send(HttpRequest request) throws IOException, InterruptedException {
|
||||
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
|
||||
int statusCode = response.statusCode();
|
||||
if (statusCode < 200 || statusCode >= 300) {
|
||||
System.out.println(response.body());
|
||||
JsonElement parsed = JsonParser.parseString(response.body());
|
||||
JsonObject object = parsed.isJsonArray() ? parsed.getAsJsonArray().get(0).getAsJsonObject() : parsed.getAsJsonObject();
|
||||
var message = "";
|
||||
if (object.has("error") && !object.get("error").isJsonNull()) {
|
||||
message = "Ошибка в запросе: " + object.get("error").getAsString();
|
||||
} else if (object.has("message") && !object.get("message").isJsonNull()) {
|
||||
message = object.get("message").getAsString();
|
||||
}
|
||||
throw new IOException(statusCode + ": " + message);
|
||||
}
|
||||
return response.body();
|
||||
}
|
||||
|
||||
public record CardAuth(String cardId, String cardToken) {
|
||||
}
|
||||
|
||||
public record CardInfo(long balance, String webhook) {
|
||||
}
|
||||
|
||||
public record PlayerCard(String name, String number) {
|
||||
}
|
||||
|
||||
public record AccountMe(String id, String username, String minecraftUuid, List<AccountCard> cards) {
|
||||
}
|
||||
|
||||
public record AccountCard(String id, String name, String number, int color) {
|
||||
}
|
||||
|
||||
public record TransactionResult(long balance) {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"id": "spmega",
|
||||
"version": "${version}",
|
||||
"name": "SPMega",
|
||||
"description": "yawaflua`s SPRadar+SPMHelper mod, thats make a lot!",
|
||||
"authors": [],
|
||||
"contact": {},
|
||||
"license": "CC-BY-SA-4.0",
|
||||
"icon": "assets/spmega/icon.png",
|
||||
"environment": "client",
|
||||
"entrypoints": {
|
||||
"fabric-datagen": [
|
||||
"git.yawaflua.tech.spmega.client.SPMegaDataGenerator"
|
||||
],
|
||||
"client": [
|
||||
"git.yawaflua.tech.spmega.client.SPMegaClient"
|
||||
],
|
||||
"main": [
|
||||
"git.yawaflua.tech.spmega.SPMega"
|
||||
]
|
||||
},
|
||||
"mixins": [
|
||||
"spmega.mixins.json",
|
||||
{
|
||||
"config": "spmega.client.mixins.json",
|
||||
"environment": "client"
|
||||
}
|
||||
],
|
||||
"depends": {
|
||||
"fabricloader": ">=${loader_version}",
|
||||
"fabric-api": "*",
|
||||
"minecraft": "${minecraft_version}",
|
||||
"cloth-config2": ">=${cloth_config_version}"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"required": true,
|
||||
"minVersion": "0.8",
|
||||
"package": "git.yawaflua.tech.spmega.mixin",
|
||||
"compatibilityLevel": "JAVA_21",
|
||||
"mixins": [
|
||||
],
|
||||
"injectors": {
|
||||
"defaultRequire": 1
|
||||
},
|
||||
"overwrites": {
|
||||
"requireAnnotations": true
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user