Author SHA1 Message Date
Dmitrii eea62f90e2 Add initial project backend structure with Docker support and configuration files. Added some useful features such as async requests, automatically adding card by message from chat etc
Release CI / build-and-upload-mod (push) Successful in 10s
Release CI / build-and-upload-mod (release) Successful in 7s
Java CI / build (push) Failing after 11m8s
Signed-off-by: Dmitrii <computer@yawaflua.tech>

Took 9 minutes
2026-06-26 03:03:26 +03:00
19 changed files with 384 additions and 27 deletions
+25
View File
@@ -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
+4
View File
@@ -0,0 +1,4 @@
SpMega.Backend/src/*
SpMega.Backend/obj/*
SpMega.Backend/appsettings.json
SpMega.Backend/SpMega.Backend.http
+23
View File
@@ -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"]
+39
View File
@@ -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();
}
}
@@ -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"
}
}
}
}
@@ -0,0 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
</PropertyGroup>
<ItemGroup>
<Content Include="..\.dockerignore">
<Link>.dockerignore</Link>
</Content>
</ItemGroup>
</Project>
@@ -0,0 +1,6 @@
@SpMega.Backend_HostAddress = http://localhost:5129
GET {{SpMega.Backend_HostAddress}}/weatherforecast/
Accept: application/json
###
@@ -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; }
}
@@ -0,0 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}
@@ -0,0 +1,9 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}
+21
View File
@@ -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
+7
View File
@@ -0,0 +1,7 @@
services:
spmega.backend:
image: spmega.backend
build:
context: .
dockerfile: SpMega.Backend/Dockerfile
@@ -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<String> 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<String> 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 "";
}
}
@@ -167,5 +167,7 @@ public class SPMegaClient implements ClientModInitializer {
} }
}); });
new ChatListener().register();
} }
} }
@@ -41,6 +41,7 @@ public class PaymentScreen extends Screen {
private Text senderCardText = Text.literal("Карта отправителя: не выбрана"); private Text senderCardText = Text.literal("Карта отправителя: не выбрана");
private ButtonWidget transferButton; private ButtonWidget transferButton;
private ButtonWidget backButton; private ButtonWidget backButton;
private boolean transferInProgress;
public PaymentScreen(Screen parent) { public PaymentScreen(Screen parent) {
this(parent, ""); this(parent, "");
@@ -192,6 +193,11 @@ public class PaymentScreen extends Screen {
} }
private void submit() { private void submit() {
if (transferInProgress) {
notifications.show(Text.literal("Перевод уже выполняется"));
return;
}
CardViewModel selectedCard = bankUiService.getSelectedCard(); CardViewModel selectedCard = bankUiService.getSelectedCard();
if (selectedCard == null) { if (selectedCard == null) {
notifications.show(Text.literal("Нет выбранной карты отправителя")); notifications.show(Text.literal("Нет выбранной карты отправителя"));
@@ -205,7 +211,8 @@ public class PaymentScreen extends Screen {
notifications.show(Text.literal("Сумма должна быть больше 0")); notifications.show(Text.literal("Сумма должна быть больше 0"));
return; return;
} }
} catch (NumberFormatException exception) { } catch (Exception exception) {
System.out.println(exception.getMessage());
notifications.show(Text.literal("Некорректная сумма")); notifications.show(Text.literal("Некорректная сумма"));
return; return;
} }
@@ -232,18 +239,46 @@ public class PaymentScreen extends Screen {
commentField.getText().trim() commentField.getText().trim()
); );
boolean accepted = bankUiService.submitPayment(draft); setTransferInProgress(true);
notifications.show(Text.literal("Отправка перевода..."));
bankUiService.submitPaymentAsync(draft)
.thenAccept(accepted -> {
if (this.client == null) {
return;
}
this.client.execute(() -> {
setTransferInProgress(false);
String serviceMessage = bankUiService.getLastMessage(); String serviceMessage = bankUiService.getLastMessage();
if (!serviceMessage.isBlank()) { if (!serviceMessage.isBlank()) {
notifications.showMessage(serviceMessage); notifications.showMessage(serviceMessage);
} else { } else {
notifications.show(accepted notifications.show(accepted
? Text.literal("Перевод выполнен") ? Text.literal("Перевод выполнен")
: Text.literal("Перевод отклонен")); : Text.literal("Перевод отклонен"));
} }
updateSenderCardSelector(); updateSenderCardSelector();
updateSenderCardText(); 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;
}
} }
private boolean isValidRecipient(String recipient) { private boolean isValidRecipient(String recipient) {
@@ -277,7 +312,7 @@ public class PaymentScreen extends Screen {
private void updateSenderCardSelector() { private void updateSenderCardSelector() {
CardViewModel selectedCard = bankUiService.getSelectedCard(); CardViewModel selectedCard = bankUiService.getSelectedCard();
if (selectedCard == null) { if (selectedCard == null) {
senderCardLabelButton.setMessage(Text.literal("\"00000\": \"0\" АР")); senderCardLabelButton.setMessage(Text.literal("TEST 00000: 0 АР"));
senderLeftButton.active = false; senderLeftButton.active = false;
senderRightButton.active = false; senderRightButton.active = false;
return; return;
@@ -285,9 +320,8 @@ public class PaymentScreen extends Screen {
senderLeftButton.active = true; senderLeftButton.active = true;
senderRightButton.active = true; senderRightButton.active = true;
String cardId = extractCardId(selectedCard.title());
String balance = Long.toString(selectedCard.balance()); String balance = Long.toString(selectedCard.balance());
senderCardLabelButton.setMessage(Text.literal("\"" + cardId + "\": \"" + balance + "\" АР")); senderCardLabelButton.setMessage(Text.literal(selectedCard.title() + ": " + balance + " АР"));
senderCardLabelButton.active = false; senderCardLabelButton.active = false;
} }
@@ -6,6 +6,7 @@ import net.minecraft.client.gui.widget.ButtonWidget;
import net.minecraft.text.Text; import net.minecraft.text.Text;
import net.minecraft.util.Util; import net.minecraft.util.Util;
import java.awt.*;
import java.net.URI; import java.net.URI;
public class QRcodeAcceptScreen extends Screen { public class QRcodeAcceptScreen extends Screen {
@@ -20,6 +21,8 @@ public class QRcodeAcceptScreen extends Screen {
@Override @Override
protected void init() { protected void init() {
this.addDrawableChild(ButtonWidget.builder(Text.translatable("button.spmega.qr.cancel"), button -> { this.addDrawableChild(ButtonWidget.builder(Text.translatable("button.spmega.qr.cancel"), button -> {
if (this.client != null) { if (this.client != null) {
this.client.setScreen(parent); this.client.setScreen(parent);
@@ -53,17 +56,10 @@ public class QRcodeAcceptScreen extends Screen {
context.drawCenteredTextWithShadow( context.drawCenteredTextWithShadow(
this.textRenderer, this.textRenderer,
Text.translatable("screen.spmega.qr.accept_link"), Text.literal("Найдена ссылка: " + url),
this.width / 2, this.width / 2,
this.height / 2 - 30, this.height / 2,
0xFFFFFF Color.WHITE.getRGB()
);
context.drawCenteredTextWithShadow(
this.textRenderer,
Text.literal(url),
this.width / 2,
this.height / 2 - 10,
0xFFD27F
); );
} }
} }
@@ -4,6 +4,7 @@ import git.yawaflua.tech.spmega.api.SPWorldsApiClient;
import net.fabricmc.loader.api.FabricLoader; import net.fabricmc.loader.api.FabricLoader;
import java.util.*; import java.util.*;
import java.util.concurrent.CompletableFuture;
public final class BankUiService { public final class BankUiService {
private static final BankUiService INSTANCE = new BankUiService(); private static final BankUiService INSTANCE = new BankUiService();
@@ -110,7 +111,7 @@ public final class BankUiService {
List<String> numbers = new ArrayList<>(); List<String> numbers = new ArrayList<>();
for (SPWorldsApiClient.PlayerCard apiCard : apiCards) { for (SPWorldsApiClient.PlayerCard apiCard : apiCards) {
if (apiCard.number() != null && !apiCard.number().isBlank()) { if (apiCard.number() != null && !apiCard.number().isBlank()) {
numbers.add(apiCard.number()); numbers.add(apiCard.name() + " : " + apiCard.number());
} }
} }
lastMessage = ""; lastMessage = "";
@@ -224,6 +225,10 @@ public final class BankUiService {
} }
} }
public CompletableFuture<Boolean> submitPaymentAsync(PaymentDraft draft) {
return CompletableFuture.supplyAsync(() -> submitPayment(draft));
}
private boolean refreshCard( private boolean refreshCard(
String cardId, String cardId,
String cardToken, String cardToken,
@@ -53,6 +53,15 @@ public final class SPWorldsApiClient {
String body = send(request); String body = send(request);
try { try {
JsonObject json = JsonParser.parseString(body).getAsJsonObject(); 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; long balance = json.has("balance") ? json.get("balance").getAsLong() : 0L;
String webhook = json.has("webhook") && !json.get("webhook").isJsonNull() String webhook = json.has("webhook") && !json.get("webhook").isJsonNull()
? json.get("webhook").getAsString() ? json.get("webhook").getAsString()
+5 -2
View File
@@ -4,8 +4,11 @@
"version": "${version}", "version": "${version}",
"name": "SPMega", "name": "SPMega",
"description": "yawaflua`s SPRadar+SPMHelper mod, thats make a lot!", "description": "yawaflua`s SPRadar+SPMHelper mod, thats make a lot!",
"authors": [], "authors": [
"contact": {}, "Dmitri 'yawaflua' Shimanski aka DOLBAYEB"
],
"contact": {
},
"license": "CC BY-NC-ND 4.0", "license": "CC BY-NC-ND 4.0",
"icon": "icon.png", "icon": "icon.png",
"environment": "client", "environment": "client",