diff --git a/src/backend/.dockerignore b/src/backend/.dockerignore
new file mode 100644
index 0000000..cd967fc
--- /dev/null
+++ b/src/backend/.dockerignore
@@ -0,0 +1,25 @@
+**/.dockerignore
+**/.env
+**/.git
+**/.gitignore
+**/.project
+**/.settings
+**/.toolstarget
+**/.vs
+**/.vscode
+**/.idea
+**/*.*proj.user
+**/*.dbmdl
+**/*.jfm
+**/azds.yaml
+**/bin
+**/charts
+**/docker-compose*
+**/Dockerfile*
+**/node_modules
+**/npm-debug.log
+**/obj
+**/secrets.dev.yaml
+**/values.dev.yaml
+LICENSE
+README.md
\ No newline at end of file
diff --git a/src/backend/.gitignore b/src/backend/.gitignore
new file mode 100644
index 0000000..74e533e
--- /dev/null
+++ b/src/backend/.gitignore
@@ -0,0 +1,4 @@
+SpMega.Backend/src/*
+SpMega.Backend/obj/*
+SpMega.Backend/appsettings.json
+SpMega.Backend/SpMega.Backend.http
\ No newline at end of file
diff --git a/src/backend/SpMega.Backend/Dockerfile b/src/backend/SpMega.Backend/Dockerfile
new file mode 100644
index 0000000..b1a0681
--- /dev/null
+++ b/src/backend/SpMega.Backend/Dockerfile
@@ -0,0 +1,23 @@
+FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS base
+USER $APP_UID
+WORKDIR /app
+EXPOSE 8080
+EXPOSE 8081
+
+FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
+ARG BUILD_CONFIGURATION=Release
+WORKDIR /src
+COPY ["SpMega.Backend/SpMega.Backend.csproj", "SpMega.Backend/"]
+RUN dotnet restore "SpMega.Backend/SpMega.Backend.csproj"
+COPY . .
+WORKDIR "/src/SpMega.Backend"
+RUN dotnet build "./SpMega.Backend.csproj" -c $BUILD_CONFIGURATION -o /app/build
+
+FROM build AS publish
+ARG BUILD_CONFIGURATION=Release
+RUN dotnet publish "./SpMega.Backend.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false
+
+FROM base AS final
+WORKDIR /app
+COPY --from=publish /app/publish .
+ENTRYPOINT ["dotnet", "SpMega.Backend.dll"]
diff --git a/src/backend/SpMega.Backend/Program.cs b/src/backend/SpMega.Backend/Program.cs
new file mode 100644
index 0000000..4f8b706
--- /dev/null
+++ b/src/backend/SpMega.Backend/Program.cs
@@ -0,0 +1,39 @@
+namespace SpMega.Backend;
+
+public class Program
+{
+ public static void Main(string[] args)
+ {
+ var builder = WebApplication.CreateBuilder(args);
+
+ // Add services to the container.
+ builder.Services.AddAuthorization();
+
+
+ var app = builder.Build();
+
+ // Configure the HTTP request pipeline.
+
+ app.UseAuthorization();
+
+ var summaries = new[]
+ {
+ "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
+ };
+
+ app.MapGet("/weatherforecast", (HttpContext httpContext) =>
+ {
+ var forecast = Enumerable.Range(1, 5).Select(index =>
+ new WeatherForecast
+ {
+ Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
+ TemperatureC = Random.Shared.Next(-20, 55),
+ Summary = summaries[Random.Shared.Next(summaries.Length)]
+ })
+ .ToArray();
+ return forecast;
+ });
+
+ app.Run();
+ }
+}
\ No newline at end of file
diff --git a/src/backend/SpMega.Backend/Properties/launchSettings.json b/src/backend/SpMega.Backend/Properties/launchSettings.json
new file mode 100644
index 0000000..1946cb0
--- /dev/null
+++ b/src/backend/SpMega.Backend/Properties/launchSettings.json
@@ -0,0 +1,14 @@
+{
+ "$schema": "https://json.schemastore.org/launchsettings.json",
+ "profiles": {
+ "http": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": false,
+ "applicationUrl": "http://localhost:5129",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ }
+ }
+}
diff --git a/src/backend/SpMega.Backend/SpMega.Backend.csproj b/src/backend/SpMega.Backend/SpMega.Backend.csproj
new file mode 100644
index 0000000..42e686b
--- /dev/null
+++ b/src/backend/SpMega.Backend/SpMega.Backend.csproj
@@ -0,0 +1,16 @@
+
+
+
+ net10.0
+ enable
+ enable
+ Linux
+
+
+
+
+ .dockerignore
+
+
+
+
diff --git a/src/backend/SpMega.Backend/SpMega.Backend.http b/src/backend/SpMega.Backend/SpMega.Backend.http
new file mode 100644
index 0000000..fc6226b
--- /dev/null
+++ b/src/backend/SpMega.Backend/SpMega.Backend.http
@@ -0,0 +1,6 @@
+@SpMega.Backend_HostAddress = http://localhost:5129
+
+GET {{SpMega.Backend_HostAddress}}/weatherforecast/
+Accept: application/json
+
+###
diff --git a/src/backend/SpMega.Backend/WeatherForecast.cs b/src/backend/SpMega.Backend/WeatherForecast.cs
new file mode 100644
index 0000000..e9d246d
--- /dev/null
+++ b/src/backend/SpMega.Backend/WeatherForecast.cs
@@ -0,0 +1,12 @@
+namespace SpMega.Backend;
+
+public class WeatherForecast
+{
+ public DateOnly Date { get; set; }
+
+ public int TemperatureC { get; set; }
+
+ public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
+
+ public string? Summary { get; set; }
+}
\ No newline at end of file
diff --git a/src/backend/SpMega.Backend/appsettings.Development.json b/src/backend/SpMega.Backend/appsettings.Development.json
new file mode 100644
index 0000000..0c208ae
--- /dev/null
+++ b/src/backend/SpMega.Backend/appsettings.Development.json
@@ -0,0 +1,8 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ }
+}
diff --git a/src/backend/SpMega.Backend/appsettings.json b/src/backend/SpMega.Backend/appsettings.json
new file mode 100644
index 0000000..10f68b8
--- /dev/null
+++ b/src/backend/SpMega.Backend/appsettings.json
@@ -0,0 +1,9 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ },
+ "AllowedHosts": "*"
+}
diff --git a/src/backend/backend.sln b/src/backend/backend.sln
new file mode 100644
index 0000000..d5efded
--- /dev/null
+++ b/src/backend/backend.sln
@@ -0,0 +1,21 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SpMega.Backend", "SpMega.Backend\SpMega.Backend.csproj", "{FF126D84-9381-4F0C-9925-FE08AC1FD07E}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{64C061F2-C216-4C79-9AC2-CC43F1C0A34D}"
+ ProjectSection(SolutionItems) = preProject
+ compose.yaml = compose.yaml
+ EndProjectSection
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {FF126D84-9381-4F0C-9925-FE08AC1FD07E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {FF126D84-9381-4F0C-9925-FE08AC1FD07E}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {FF126D84-9381-4F0C-9925-FE08AC1FD07E}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {FF126D84-9381-4F0C-9925-FE08AC1FD07E}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+EndGlobal
diff --git a/src/backend/compose.yaml b/src/backend/compose.yaml
new file mode 100644
index 0000000..103676e
--- /dev/null
+++ b/src/backend/compose.yaml
@@ -0,0 +1,7 @@
+services:
+ spmega.backend:
+ image: spmega.backend
+ build:
+ context: .
+ dockerfile: SpMega.Backend/Dockerfile
+
diff --git a/src/client/java/git/yawaflua/tech/spmega/client/ChatListener.java b/src/client/java/git/yawaflua/tech/spmega/client/ChatListener.java
new file mode 100644
index 0000000..f1d6cec
--- /dev/null
+++ b/src/client/java/git/yawaflua/tech/spmega/client/ChatListener.java
@@ -0,0 +1,124 @@
+package git.yawaflua.tech.spmega.client;
+
+import git.yawaflua.tech.spmega.client.ui.UiNotifications;
+import git.yawaflua.tech.spmega.client.ui.service.BankUiService;
+import net.fabricmc.fabric.api.client.message.v1.ClientReceiveMessageEvents;
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.text.ClickEvent;
+import net.minecraft.text.Text;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.CompletableFuture;
+import java.util.regex.Pattern;
+
+public class ChatListener {
+ private final UiNotifications notifications = UiNotifications.instance();
+ private final Pattern TRANSACTION_PATTERN = Pattern.compile(
+ ".*(Управление картой).*",
+ Pattern.CASE_INSENSITIVE
+ );
+
+ public void register() {
+ ClientReceiveMessageEvents.CHAT.register((message, signedMessage, sender, params, timestamp) -> {
+ try {
+ handleMessage(message);
+ } catch (Throwable t) {
+ System.err.println("[SPMega] Ошибка обработки сообщения в CHAT листенере:");
+ t.printStackTrace();
+ }
+ });
+
+ ClientReceiveMessageEvents.GAME.register((message, overlay) -> {
+ try {
+ handleMessage(message);
+ } catch (Throwable t) {
+ System.err.println("[SPMega] Ошибка обработки сообщения в GAME листенере:");
+ t.printStackTrace();
+ }
+ });
+ }
+
+ private void handleMessage(Text message) {
+ if (message == null) {
+ return;
+ }
+
+ String text = message.getString();
+ if (TRANSACTION_PATTERN.matcher(text).matches()) {
+ List clickValues = new ArrayList<>();
+ collectClickEventValues(message, clickValues);
+
+ if (clickValues.size() >= 2) {
+ String tokenId = clickValues.get(0);
+ String cardId = clickValues.get(1);
+
+ MinecraftClient client = MinecraftClient.getInstance();
+ if (client.player != null) {
+ String playerUuid = client.player.getUuidAsString();
+
+ CompletableFuture
+ .supplyAsync(() -> BankUiService.instance().addCard(cardId, tokenId, playerUuid))
+ .thenAccept(msg -> {
+ if (client == null) {
+ return;
+ }
+ client.execute(() -> {
+ notifications.showMessage(msg);
+ });
+ })
+ .exceptionally(exception -> {
+ if (client != null) {
+ client.execute(() -> {
+ notifications.show(Text.literal("Ошибка добавления карты: " + exception.getMessage()));
+ });
+ }
+ return null;
+ });
+ }
+ } else {
+ System.out.println("[SPMega] Недостаточно кликабельных данных в сообщении (найдено: " + clickValues.size() + ")");
+ }
+ }
+ }
+
+ private void collectClickEventValues(Text text, List values) {
+ if (text == null) {
+ return;
+ }
+
+ ClickEvent clickEvent = text.getStyle().getClickEvent();
+ if (clickEvent != null) {
+ try {
+ String value = getEventValue(clickEvent);
+ if (value != null && !value.isEmpty()) {
+ values.add(value);
+ }
+ } catch (Throwable t) {
+ System.err.println("[SPMega] Ошибка извлечения значения клик-события:");
+ t.printStackTrace();
+ }
+ }
+
+ for (Text sibling : text.getSiblings()) {
+ collectClickEventValues(sibling, values);
+ }
+ }
+
+ private String getEventValue(ClickEvent clickEvent) {
+ if (clickEvent instanceof ClickEvent.CopyToClipboard(String value)) {
+ return value;
+ } else if (clickEvent instanceof ClickEvent.RunCommand(String command1)) {
+ return command1;
+ } else if (clickEvent instanceof ClickEvent.SuggestCommand(String command)) {
+ return command;
+ } else if (clickEvent instanceof ClickEvent.OpenUrl(java.net.URI uri)) {
+ return uri.toString();
+ } else if (clickEvent instanceof ClickEvent.OpenFile(String path)) {
+ return path;
+ } else if (clickEvent instanceof ClickEvent.ChangePage(int page)) {
+ return String.valueOf(page);
+ }
+ return "";
+ }
+}
diff --git a/src/client/java/git/yawaflua/tech/spmega/client/SPMegaClient.java b/src/client/java/git/yawaflua/tech/spmega/client/SPMegaClient.java
index ee06332..a023d27 100644
--- a/src/client/java/git/yawaflua/tech/spmega/client/SPMegaClient.java
+++ b/src/client/java/git/yawaflua/tech/spmega/client/SPMegaClient.java
@@ -167,5 +167,7 @@ public class SPMegaClient implements ClientModInitializer {
}
});
+
+ new ChatListener().register();
}
}
diff --git a/src/client/java/git/yawaflua/tech/spmega/client/ui/PaymentScreen.java b/src/client/java/git/yawaflua/tech/spmega/client/ui/PaymentScreen.java
index 668b13f..78129eb 100644
--- a/src/client/java/git/yawaflua/tech/spmega/client/ui/PaymentScreen.java
+++ b/src/client/java/git/yawaflua/tech/spmega/client/ui/PaymentScreen.java
@@ -41,6 +41,7 @@ public class PaymentScreen extends Screen {
private Text senderCardText = Text.literal("Карта отправителя: не выбрана");
private ButtonWidget transferButton;
private ButtonWidget backButton;
+ private boolean transferInProgress;
public PaymentScreen(Screen parent) {
this(parent, "");
@@ -192,6 +193,11 @@ public class PaymentScreen extends Screen {
}
private void submit() {
+ if (transferInProgress) {
+ notifications.show(Text.literal("Перевод уже выполняется"));
+ return;
+ }
+
CardViewModel selectedCard = bankUiService.getSelectedCard();
if (selectedCard == null) {
notifications.show(Text.literal("Нет выбранной карты отправителя"));
@@ -205,7 +211,8 @@ public class PaymentScreen extends Screen {
notifications.show(Text.literal("Сумма должна быть больше 0"));
return;
}
- } catch (NumberFormatException exception) {
+ } catch (Exception exception) {
+ System.out.println(exception.getMessage());
notifications.show(Text.literal("Некорректная сумма"));
return;
}
@@ -232,18 +239,46 @@ public class PaymentScreen extends Screen {
commentField.getText().trim()
);
- boolean accepted = bankUiService.submitPayment(draft);
- String serviceMessage = bankUiService.getLastMessage();
- if (!serviceMessage.isBlank()) {
+ setTransferInProgress(true);
+ notifications.show(Text.literal("Отправка перевода..."));
- notifications.showMessage(serviceMessage);
- } else {
- notifications.show(accepted
- ? Text.literal("Перевод выполнен")
- : Text.literal("Перевод отклонен"));
+ bankUiService.submitPaymentAsync(draft)
+ .thenAccept(accepted -> {
+ if (this.client == null) {
+ return;
+ }
+ this.client.execute(() -> {
+ setTransferInProgress(false);
+
+ String serviceMessage = bankUiService.getLastMessage();
+ if (!serviceMessage.isBlank()) {
+ notifications.showMessage(serviceMessage);
+ } else {
+ notifications.show(accepted
+ ? Text.literal("Перевод выполнен")
+ : Text.literal("Перевод отклонен"));
+ }
+
+ updateSenderCardSelector();
+ updateSenderCardText();
+ });
+ })
+ .exceptionally(exception -> {
+ if (this.client != null) {
+ this.client.execute(() -> {
+ setTransferInProgress(false);
+ notifications.show(Text.literal("Ошибка перевода: " + exception.getMessage()));
+ });
+ }
+ return null;
+ });
+ }
+
+ private void setTransferInProgress(boolean inProgress) {
+ transferInProgress = inProgress;
+ if (transferButton != null) {
+ transferButton.active = !inProgress;
}
- updateSenderCardSelector();
- updateSenderCardText();
}
private boolean isValidRecipient(String recipient) {
@@ -277,7 +312,7 @@ public class PaymentScreen extends Screen {
private void updateSenderCardSelector() {
CardViewModel selectedCard = bankUiService.getSelectedCard();
if (selectedCard == null) {
- senderCardLabelButton.setMessage(Text.literal("\"00000\": \"0\" АР"));
+ senderCardLabelButton.setMessage(Text.literal("TEST 00000: 0 АР"));
senderLeftButton.active = false;
senderRightButton.active = false;
return;
@@ -285,9 +320,8 @@ public class PaymentScreen extends Screen {
senderLeftButton.active = true;
senderRightButton.active = true;
- String cardId = extractCardId(selectedCard.title());
String balance = Long.toString(selectedCard.balance());
- senderCardLabelButton.setMessage(Text.literal("\"" + cardId + "\": \"" + balance + "\" АР"));
+ senderCardLabelButton.setMessage(Text.literal(selectedCard.title() + ": " + balance + " АР"));
senderCardLabelButton.active = false;
}
diff --git a/src/client/java/git/yawaflua/tech/spmega/client/ui/QRcodeAcceptScreen.java b/src/client/java/git/yawaflua/tech/spmega/client/ui/QRcodeAcceptScreen.java
index df15417..72d7229 100644
--- a/src/client/java/git/yawaflua/tech/spmega/client/ui/QRcodeAcceptScreen.java
+++ b/src/client/java/git/yawaflua/tech/spmega/client/ui/QRcodeAcceptScreen.java
@@ -6,6 +6,7 @@ import net.minecraft.client.gui.widget.ButtonWidget;
import net.minecraft.text.Text;
import net.minecraft.util.Util;
+import java.awt.*;
import java.net.URI;
public class QRcodeAcceptScreen extends Screen {
@@ -20,6 +21,8 @@ public class QRcodeAcceptScreen extends Screen {
@Override
protected void init() {
+
+
this.addDrawableChild(ButtonWidget.builder(Text.translatable("button.spmega.qr.cancel"), button -> {
if (this.client != null) {
this.client.setScreen(parent);
@@ -53,17 +56,10 @@ public class QRcodeAcceptScreen extends Screen {
context.drawCenteredTextWithShadow(
this.textRenderer,
- Text.translatable("screen.spmega.qr.accept_link"),
+ Text.literal("Найдена ссылка: " + url),
this.width / 2,
- this.height / 2 - 30,
- 0xFFFFFF
- );
- context.drawCenteredTextWithShadow(
- this.textRenderer,
- Text.literal(url),
- this.width / 2,
- this.height / 2 - 10,
- 0xFFD27F
+ this.height / 2,
+ Color.WHITE.getRGB()
);
}
}
diff --git a/src/client/java/git/yawaflua/tech/spmega/client/ui/service/BankUiService.java b/src/client/java/git/yawaflua/tech/spmega/client/ui/service/BankUiService.java
index a458a09..1b060e5 100644
--- a/src/client/java/git/yawaflua/tech/spmega/client/ui/service/BankUiService.java
+++ b/src/client/java/git/yawaflua/tech/spmega/client/ui/service/BankUiService.java
@@ -4,6 +4,7 @@ import git.yawaflua.tech.spmega.api.SPWorldsApiClient;
import net.fabricmc.loader.api.FabricLoader;
import java.util.*;
+import java.util.concurrent.CompletableFuture;
public final class BankUiService {
private static final BankUiService INSTANCE = new BankUiService();
@@ -110,7 +111,7 @@ public final class BankUiService {
List numbers = new ArrayList<>();
for (SPWorldsApiClient.PlayerCard apiCard : apiCards) {
if (apiCard.number() != null && !apiCard.number().isBlank()) {
- numbers.add(apiCard.number());
+ numbers.add(apiCard.name() + " : " + apiCard.number());
}
}
lastMessage = "";
@@ -224,6 +225,10 @@ public final class BankUiService {
}
}
+ public CompletableFuture submitPaymentAsync(PaymentDraft draft) {
+ return CompletableFuture.supplyAsync(() -> submitPayment(draft));
+ }
+
private boolean refreshCard(
String cardId,
String cardToken,
diff --git a/src/main/java/git/yawaflua/tech/spmega/api/SPWorldsApiClient.java b/src/main/java/git/yawaflua/tech/spmega/api/SPWorldsApiClient.java
index af6a208..61f95be 100644
--- a/src/main/java/git/yawaflua/tech/spmega/api/SPWorldsApiClient.java
+++ b/src/main/java/git/yawaflua/tech/spmega/api/SPWorldsApiClient.java
@@ -53,6 +53,15 @@ public final class SPWorldsApiClient {
String body = send(request);
try {
JsonObject json = JsonParser.parseString(body).getAsJsonObject();
+ if (json.has("statusCode")) {
+ switch (json.get("statusCode").getAsInt()) {
+ case 403:
+ throw new IOException("Апи вернула ошибку: " + json.get("message").getAsString());
+ default:
+ System.out.println("Unhandled status code in card info response: " + json.get("statusCode").getAsInt());
+ break;
+ }
+ }
long balance = json.has("balance") ? json.get("balance").getAsLong() : 0L;
String webhook = json.has("webhook") && !json.get("webhook").isJsonNull()
? json.get("webhook").getAsString()
diff --git a/src/main/resources/fabric.mod.json b/src/main/resources/fabric.mod.json
index d5e984e..2beddca 100644
--- a/src/main/resources/fabric.mod.json
+++ b/src/main/resources/fabric.mod.json
@@ -4,8 +4,11 @@
"version": "${version}",
"name": "SPMega",
"description": "yawaflua`s SPRadar+SPMHelper mod, thats make a lot!",
- "authors": [],
- "contact": {},
+ "authors": [
+ "Dmitri 'yawaflua' Shimanski aka DOLBAYEB"
+ ],
+ "contact": {
+ },
"license": "CC BY-NC-ND 4.0",
"icon": "icon.png",
"environment": "client",