Add backend structure with Docker support and initial configuration files
Java CI / build (push) Successful in 1m56s

Signed-off-by: Dmitrii <computer@yawaflua.tech>

Took 4 hours 25 minutes
This commit is contained in:
Dmitrii
2026-06-28 08:20:33 +03:00
parent 7862597f45
commit eee0d44c92
46 changed files with 3141 additions and 125 deletions
-25
View File
@@ -1,25 +0,0 @@
**/.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
@@ -1,4 +0,0 @@
SpMega.Backend/src/*
SpMega.Backend/obj/*
SpMega.Backend/appsettings.json
SpMega.Backend/SpMega.Backend.http
-23
View File
@@ -1,23 +0,0 @@
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
@@ -1,39 +0,0 @@
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();
}
}
@@ -1,14 +0,0 @@
{
"$schema": "https://json.schemastore.org/launchsettings.json",
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": false,
"applicationUrl": "http://localhost:5129",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}
@@ -1,16 +0,0 @@
<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>
@@ -1,6 +0,0 @@
@SpMega.Backend_HostAddress = http://localhost:5129
GET {{SpMega.Backend_HostAddress}}/weatherforecast/
Accept: application/json
###
@@ -1,12 +0,0 @@
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; }
}
@@ -1,8 +0,0 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}
@@ -1,9 +0,0 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}
-21
View File
@@ -1,21 +0,0 @@
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
@@ -1,7 +0,0 @@
services:
spmega.backend:
image: spmega.backend
build:
context: .
dockerfile: SpMega.Backend/Dockerfile
@@ -0,0 +1,102 @@
package git.yawaflua.tech.spmega.client;
import com.terraformersmc.modmenu.api.ConfigScreenFactory;
import com.terraformersmc.modmenu.api.ModMenuApi;
import git.yawaflua.tech.spmega.GpsHudPosition;
import git.yawaflua.tech.spmega.ModConfig;
import git.yawaflua.tech.spmega.SPMega;
import git.yawaflua.tech.spmega.client.ui.service.BackendAuthenticator;
import me.shedaniel.clothconfig2.api.ConfigBuilder;
import me.shedaniel.clothconfig2.api.ConfigCategory;
import me.shedaniel.clothconfig2.api.ConfigEntryBuilder;
import net.minecraft.client.MinecraftClient;
import net.minecraft.text.Text;
import net.minecraft.util.Formatting;
public class ModMenuIntegration implements ModMenuApi {
@Override
public ConfigScreenFactory<?> getModConfigScreenFactory() {
return parent -> {
ConfigBuilder builder = ConfigBuilder.create()
.setParentScreen(parent)
.setTitle(Text.translatable("title.spmega.config"));
ConfigEntryBuilder entryBuilder = builder.entryBuilder();
ConfigCategory general = builder.getOrCreateCategory(Text.translatable("category.spmega.general"));
ModConfig current = SPMega.getConfig();
if (current == null) {
current = ModConfig.createDefault();
}
final String[] apiDomain = {current.apiDomain()};
final String[] apiToken = {current.apiToken()};
final boolean[] allowBackend = {current.allowBackend()};
final boolean[] signQuickPayEnabled = {current.signQuickPayEnabled()};
final GpsHudPosition[] gpsPosition = {current.gpsPosition()};
general.addEntry(entryBuilder.startStrField(Text.translatable("option.spmega.api_domain"), current.apiDomain())
.setDefaultValue(ModConfig.DEFAULT_API_DOMAIN)
.setSaveConsumer(newValue -> apiDomain[0] = newValue)
.build());
general.addEntry(entryBuilder.startStrField(Text.translatable("option.spmega.api_token"), current.apiToken())
.setDefaultValue(ModConfig.DEFAULT_API_TOKEN)
.setSaveConsumer(newValue -> apiToken[0] = newValue)
.build());
general.addEntry(entryBuilder.startBooleanToggle(Text.translatable("option.spmega.allow_backend"), current.allowBackend())
.setDefaultValue(ModConfig.ALLOW_BACKEND)
.setSaveConsumer(newValue -> allowBackend[0] = newValue)
.build());
boolean isLoggedIn = !current.apiToken().isEmpty() && !current.apiToken().equals(ModConfig.DEFAULT_API_TOKEN);
general.addEntry(entryBuilder.startBooleanToggle(Text.literal("Авторизоваться").formatted(Formatting.RED), isLoggedIn)
.setTooltip(Text.literal("Запустить процесс авторизации через Mojang и ваш бэкенд"))
.setSaveConsumer(newValue -> {
if (newValue) {
BackendAuthenticator.authenticate(MinecraftClient.getInstance());
if (SPMega.getConfig() != null) {
apiToken[0] = SPMega.getConfig().apiToken();
}
} else {
apiToken[0] = ModConfig.DEFAULT_API_TOKEN;
}
})
.build());
general.addEntry(entryBuilder.startBooleanToggle(Text.translatable("option.spmega.sign_quick_pay"), current.signQuickPayEnabled())
.setDefaultValue(ModConfig.DEFAULT_SIGN_QUICK_PAY_ENABLED)
.setSaveConsumer(newValue -> signQuickPayEnabled[0] = newValue)
.build());
general.addEntry(entryBuilder.startEnumSelector(
Text.translatable("option.spmega.gps_position"),
GpsHudPosition.class,
current.gpsPosition()
)
.setDefaultValue(ModConfig.DEFAULT_GPS_POSITION)
.setEnumNameProvider(position -> Text.translatable("option.spmega.gps_position." + position.name().toLowerCase()))
.setSaveConsumer(newValue -> gpsPosition[0] = newValue)
.build());
builder.setSavingRunnable(() -> {
boolean gpsEnabledVal = true;
if (SPMega.getConfig() != null) {
gpsEnabledVal = SPMega.getConfig().gpsEnabled();
}
ModConfig updated = new ModConfig(
apiDomain[0],
apiToken[0],
allowBackend[0],
signQuickPayEnabled[0],
gpsEnabledVal,
gpsPosition[0]
);
SPMega.setConfig(updated);
});
return builder.build();
};
}
}
@@ -1,9 +1,12 @@
package git.yawaflua.tech.spmega.client;
import git.yawaflua.tech.spmega.ModConfig;
import git.yawaflua.tech.spmega.SPMega;
import git.yawaflua.tech.spmega.client.qr.QRCodeScanner;
import git.yawaflua.tech.spmega.client.ui.GpsHudRenderer;
import git.yawaflua.tech.spmega.client.ui.UiNotifications;
import git.yawaflua.tech.spmega.client.ui.UiOpeners;
import git.yawaflua.tech.spmega.client.ui.service.BackendAuthenticator;
import git.yawaflua.tech.spmega.client.ui.service.BankUiService;
import net.fabricmc.api.ClientModInitializer;
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
@@ -18,6 +21,7 @@ import net.minecraft.client.MinecraftClient;
import net.minecraft.client.option.KeyBinding;
import net.minecraft.client.util.InputUtil;
import net.minecraft.entity.decoration.ItemFrameEntity;
import net.minecraft.entity.player.PlayerEntity;
import net.minecraft.text.Text;
import net.minecraft.util.ActionResult;
import org.lwjgl.glfw.GLFW;
@@ -29,6 +33,9 @@ public class SPMegaClient implements ClientModInitializer {
private static final Pattern ISOLATED_FIVE_DIGITS = Pattern.compile("(?<!\\d)(\\d{5})(?!\\d)");
private static KeyBinding openBankMenuKeyBinding;
private static KeyBinding scanQrKeyBinding;
private static KeyBinding toggleGpsKeyBinding;
private static boolean wasPaymentShortcutPressed = false;
private static String extractCardNumber(SignBlockEntity signBlockEntity) {
String candidate = findFiveDigits(signBlockEntity.getFrontText().getMessages(false));
@@ -65,6 +72,7 @@ public class SPMegaClient implements ClientModInitializer {
@Override
public void onInitializeClient() {
GpsHudRenderer.instance();
openBankMenuKeyBinding = KeyBindingHelper.registerKeyBinding(new KeyBinding(
"key.spmega.open_menu",
InputUtil.Type.KEYSYM,
@@ -79,6 +87,17 @@ public class SPMegaClient implements ClientModInitializer {
KeyBinding.Category.GAMEPLAY
));
toggleGpsKeyBinding = KeyBindingHelper.registerKeyBinding(new KeyBinding(
"key.spmega.toggle_gps",
InputUtil.Type.KEYSYM,
GLFW.GLFW_KEY_J,
KeyBinding.Category.GAMEPLAY
));
if (SPMega.getConfig() != null) {
GpsHudRenderer.instance().setEnabled(SPMega.getConfig().gpsEnabled());
}
ClientTickEvents.END_CLIENT_TICK.register(client -> {
UiNotifications.instance().tick();
while (openBankMenuKeyBinding.wasPressed()) {
@@ -87,9 +106,40 @@ public class SPMegaClient implements ClientModInitializer {
while (scanQrKeyBinding.wasPressed()) {
QRCodeScanner.ScanQrCode(client);
}
while (toggleGpsKeyBinding.wasPressed()) {
GpsHudRenderer.instance().toggle();
if (SPMega.getConfig() != null) {
ModConfig current = SPMega.getConfig();
ModConfig updated = new ModConfig(
current.apiDomain(),
current.apiToken(),
current.allowBackend(),
current.signQuickPayEnabled(),
GpsHudRenderer.instance().isEnabled(),
current.gpsPosition()
);
SPMega.setConfig(updated);
}
String status = GpsHudRenderer.instance().isEnabled() ? "включен" : "выключен";
UiNotifications.instance().show(Text.literal("GPS Ада " + status));
}
if (client.player != null && client.options != null) {
boolean isCombinationPressed = client.options.sprintKey.isPressed() && client.options.sneakKey.isPressed() && client.options.leftKey.isPressed();
if (isCombinationPressed) {
if (!wasPaymentShortcutPressed && client.currentScreen == null) {
if (client.targetedEntity instanceof PlayerEntity targetedPlayer && targetedPlayer != client.player) {
String recipient = targetedPlayer.getStringifiedName();
UiOpeners.openPaymentMenu(client, recipient);
}
}
wasPaymentShortcutPressed = true;
} else {
wasPaymentShortcutPressed = false;
}
}
});
// World HUD path (when no screen is open).
HudRenderCallback.EVENT.register((drawContext, tickDeltaManager) -> {
MinecraftClient client = MinecraftClient.getInstance();
if (client.currentScreen != null || client.textRenderer == null) {
@@ -101,16 +151,35 @@ public class SPMegaClient implements ClientModInitializer {
client.getWindow().getScaledWidth(),
client.getWindow().getScaledHeight()
);
GpsHudRenderer.instance().render(
drawContext,
client.textRenderer,
client.getWindow().getScaledWidth(),
client.getWindow().getScaledHeight()
);
});
// Screen path (for all GUI screens).
ScreenEvents.AFTER_INIT.register((client, screen, scaledWidth, scaledHeight) ->
ScreenEvents.afterRender(screen).register((screenInstance, drawContext, mouseX, mouseY, tickDelta) -> {
if (client.textRenderer == null) {
return;
}
UiNotifications.instance().render(drawContext, client.textRenderer, scaledWidth, scaledHeight);
})
ScreenEvents.AFTER_INIT.register((client, screen, scaledWidth, scaledHeight) -> {
ScreenEvents.afterRender(screen).register((screenInstance, drawContext, mouseX, mouseY, tickDelta) -> {
if (client.textRenderer == null) {
return;
}
UiNotifications.instance().render(drawContext, client.textRenderer, scaledWidth, scaledHeight);
});
// CompletableFuture.supplyAsync(() -> BackendAuthenticator.authenticate(client))
// .thenAccept(authResult -> {
// if (authResult) {
// System.out.println("SPMega: Authenticated on backend successfully.");
// } else {
// System.err.println("SPMega: Authentication on backend failed");
// }
// })
// .exceptionally(ex -> {
// System.err.println("SPMega: Authentication error: " + ex.getMessage());
// return null;
// });
}
);
ClientPlayConnectionEvents.JOIN.register((handler, sender, client) -> {
@@ -169,5 +238,8 @@ public class SPMegaClient implements ClientModInitializer {
});
new ChatListener().register();
BackendAuthenticator.authenticate(MinecraftClient.getInstance());
System.out.println("Author of SPMega make it with 4 cans of monster");
System.out.println("Initialized beshalom! Tieie tovim!");
}
}
@@ -9,12 +9,14 @@ import net.minecraft.text.Text;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CompletableFuture;
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<>();
private ButtonWidget historyButton;
public CardScreen(Screen parent) {
super(Text.literal("Управление картами"));
@@ -44,16 +46,22 @@ public class CardScreen extends Screen {
}
this.addDrawableChild(ButtonWidget.builder(Text.literal("Удалить"), button -> {
bankUiService.removeSelectedCard();
CompletableFuture.supplyAsync(() -> {
bankUiService.removeSelectedCard();
return true;
});
notifications.showMessage(bankUiService.getLastMessage());
this.clearAndInit();
}).dimensions(rightX, startY, columnWidth, 20).build());
this.addDrawableChild(ButtonWidget.builder(Text.literal("Изменить"), button -> {
this.addDrawableChild(ButtonWidget.builder(Text.literal("Обновить"), button -> {
String playerUuid = this.client != null && this.client.player != null
? this.client.player.getUuidAsString()
: "";
bankUiService.refreshSelectedCard(playerUuid);
CompletableFuture.supplyAsync(() -> {
bankUiService.refreshSelectedCard(playerUuid);
return true;
});
String message = bankUiService.getLastMessage().isBlank() ? "Карта обновлена" : bankUiService.getLastMessage();
notifications.showMessage(message);
this.clearAndInit();
@@ -69,6 +77,18 @@ public class CardScreen extends Screen {
}));
}).dimensions(rightX, startY + 48, columnWidth, 20).build());
historyButton = this.addDrawableChild(ButtonWidget.builder(Text.literal("История"), button -> {
if (this.client == null) {
return;
}
CardViewModel selected = bankUiService.getSelectedCard();
if (selected == null) {
notifications.show(Text.literal("Сначала выбери или добавь карту"));
return;
}
this.client.setScreen(new TransactionHistoryScreen(this, selected.id(), selected.title()));
}).dimensions(rightX, startY + 72, columnWidth, 20).build());
this.addDrawableChild(ButtonWidget.builder(Text.literal("Назад"), button -> this.close())
.dimensions(rightX, startY + 120, columnWidth, 20)
.build());
@@ -119,5 +139,9 @@ public class CardScreen extends Screen {
button.setMessage(Text.literal(prefix + cards.get(i).title() + suffix));
button.active = !selected;
}
if (historyButton != null) {
historyButton.active = !cards.isEmpty();
}
}
}
@@ -0,0 +1,238 @@
package git.yawaflua.tech.spmega.client.ui;
import com.google.gson.Gson;
import git.yawaflua.tech.spmega.GpsHudPosition;
import git.yawaflua.tech.spmega.SPMega;
import net.minecraft.client.MinecraftClient;
import net.minecraft.client.font.TextRenderer;
import net.minecraft.client.gui.DrawContext;
import net.minecraft.text.Text;
import net.minecraft.world.World;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
public final class GpsHudRenderer {
private static final GpsHudRenderer INSTANCE = new GpsHudRenderer();
private final Object citiesLock = new Object();
private boolean enabled = true;
private List<City> cities = new ArrayList<>();
private GpsHudRenderer() {
ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(r -> {
Thread thread = new Thread(r, "SPMega-City-Poller");
thread.setDaemon(true);
return thread;
});
scheduler.scheduleAtFixedRate(this::fetchCities, 0, 5, TimeUnit.HOURS);
}
public static GpsHudRenderer instance() {
return INSTANCE;
}
public boolean isEnabled() {
return enabled;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
public void toggle() {
this.enabled = !this.enabled;
}
private void fetchCities() {
try {
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://spm-map.tonyaleksandr.ru/api/map/territories"))
.GET()
.build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() == 200) {
TerritoryWrapper[] parsed = new Gson().fromJson(response.body(), TerritoryWrapper[].class);
if (parsed != null) {
List<City> list = new ArrayList<>();
for (TerritoryWrapper w : parsed) {
if (w != null && w.territory != null && w.territory.nether_portal != null && w.territory.nether_portal.length >= 2) {
City c = new City();
c.name = w.territory.name;
c.netherX = w.territory.nether_portal[0];
c.netherZ = w.territory.nether_portal[1];
list.add(c);
}
}
synchronized (citiesLock) {
cities = list;
}
}
}
} catch (Exception ignored) {
}
}
public void render(DrawContext context, TextRenderer textRenderer, int width, int height) {
if (!enabled) {
return;
}
MinecraftClient client = MinecraftClient.getInstance();
if (client.world == null || client.player == null) {
return;
}
if (client.world.getRegistryKey() != World.NETHER) {
return;
}
double playerX = client.player.getX();
double playerZ = client.player.getZ();
LaneInfo playerLane = new LaneInfo(Math.round(playerX), Math.round(playerZ));
String titleText = "§7Ветка: " + playerLane.name + " §7(§f" + playerLane.coord + "§7)";
String offsetText = "§7Смещение: §f" + (playerLane.offset == 0 ? "0" : (playerLane.offset > 0 ? "+" + playerLane.offset : playerLane.offset));
City nearestCity = null;
double minDistanceSq = Double.MAX_VALUE;
for (City city : cities) {
double dx = playerX - (city.netherX / 8);
double dz = playerZ - (city.netherZ / 8);
double distSq = Math.sqrt(dx * dx + dz * dz);
if (distSq < minDistanceSq) {
minDistanceSq = distSq;
nearestCity = city;
}
}
String cityText = null;
if (nearestCity != null) {
long cx = Math.round(nearestCity.netherX / 8.0);
long cz = Math.round(nearestCity.netherZ / 8.0);
LaneInfo cityLane = new LaneInfo(cx, cz);
cityText = "§7Ближайший: §f" + nearestCity.name + " §7~(§f" + cityLane.name + " §f" + cityLane.coord + "§7, §7см: §f" + (cityLane.offset == 0 ? "0" : (cityLane.offset > 0 ? "+" + cityLane.offset : cityLane.offset)) + "§7)";
}
int padding = 6;
int lineSpacing = 2;
int textWidth = Math.max(textRenderer.getWidth(Text.literal(titleText)), textRenderer.getWidth(Text.literal(offsetText)));
if (cityText != null) {
textWidth = Math.max(textWidth, textRenderer.getWidth(Text.literal(cityText)));
}
int boxWidth = textWidth + padding * 2 + 6;
int linesCount = cityText != null ? 3 : 2;
int boxHeight = textRenderer.fontHeight * linesCount + lineSpacing * (linesCount - 1) + padding * 2;
GpsHudPosition position = GpsHudPosition.TOP_CENTER;
if (SPMega.getConfig() != null) {
position = SPMega.getConfig().gpsPosition();
}
int boxX;
int boxY;
switch (position) {
case TOP_RIGHT:
boxX = width - boxWidth - 10;
boxY = 10;
break;
case BOTTOM_LEFT:
boxX = 10;
boxY = height - boxHeight - 10;
break;
case BOTTOM_RIGHT:
boxX = width - boxWidth - 10;
boxY = height - boxHeight - 10;
break;
case TOP_CENTER:
boxX = (width - boxWidth) / 2;
boxY = 10;
break;
case BOTTOM_CENTER:
boxX = (width - boxWidth) / 2;
boxY = height - boxHeight - 10;
break;
case TOP_LEFT:
default:
boxX = 10;
boxY = 10;
break;
}
int bgCol = 0x90101010;
context.fill(boxX, boxY, boxX + boxWidth, boxY + boxHeight, bgCol);
int barWidth = 3;
context.fill(boxX + padding, boxY + padding, boxX + padding + barWidth, boxY + boxHeight - padding, playerLane.color);
int textX = boxX + padding + barWidth + 4;
int textY = boxY + padding;
context.drawTextWithShadow(textRenderer, Text.literal(titleText), textX, textY, 0xFFFFFFFF);
context.drawTextWithShadow(textRenderer, Text.literal(offsetText), textX, textY + textRenderer.fontHeight + lineSpacing, 0xFFFFFFFF);
if (cityText != null) {
context.drawTextWithShadow(textRenderer, Text.literal(cityText), textX, textY + (textRenderer.fontHeight + lineSpacing) * 2, 0xFFFFFFFF);
}
}
private static class City {
String name;
int netherX;
int netherZ;
}
private static class TerritoryWrapper {
Territory territory;
}
private static class Territory {
String name;
int[] nether_portal;
}
private static class LaneInfo {
String name;
int color;
long coord;
long offset;
LaneInfo(long x, long z) {
long absX = Math.abs(x);
long absZ = Math.abs(z);
if (absX > absZ) {
if (x > 0) {
name = "§aЗеленая";
color = 0xFF55FF55;
coord = x;
offset = z;
} else {
name = "§9Синяя";
color = 0xFF5555FF;
coord = absX;
offset = z;
}
} else {
if (z < 0) {
name = "§cКрасная";
color = 0xFFFF5555;
coord = absZ;
offset = x;
} else {
name = "§eЖелтая";
color = 0xFFFFFF55;
coord = absZ;
offset = x;
}
}
}
}
}
@@ -0,0 +1,124 @@
package git.yawaflua.tech.spmega.client.ui;
import git.yawaflua.tech.spmega.client.ui.service.BackendAuthenticator;
import git.yawaflua.tech.spmega.client.ui.service.BankDatabase.LocalTransaction;
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.text.Text;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CompletableFuture;
public class TransactionHistoryScreen extends Screen {
private final Screen parent;
private final String cardId;
private final String cardTitle;
private final List<LocalTransaction> transactions = new ArrayList<>();
private boolean loading = true;
private String errorMessage = "";
public TransactionHistoryScreen(Screen parent, String cardId, String cardTitle) {
super(Text.literal("История транзакций"));
this.parent = parent;
this.cardId = cardId;
this.cardTitle = cardTitle;
}
@Override
protected void init() {
int centerX = this.width / 2;
int startY = this.height / 2 + 50;
this.addDrawableChild(ButtonWidget.builder(Text.literal("Назад"), button -> this.close())
.dimensions(centerX - 60, startY + 10, 120, 20)
.build());
loadTransactions();
}
private void loadTransactions() {
loading = true;
errorMessage = "";
transactions.clear();
CompletableFuture.runAsync(() -> {
try {
List<LocalTransaction> list = null;
git.yawaflua.tech.spmega.ModConfig config = git.yawaflua.tech.spmega.SPMega.getConfig();
if (config != null && config.allowBackend()) {
try {
list = BackendAuthenticator.fetchTransactionsFromBackend(cardId);
} catch (Exception e) {
System.err.println("[SPMEGA] Failed to fetch transactions from server, falling back to DB: " + e.getMessage());
}
}
if (list == null) {
list = BankUiService.instance().getDatabase().loadTransferHistory(cardId);
}
List<LocalTransaction> finalList = list;
if (this.client != null) {
this.client.execute(() -> {
this.transactions.addAll(finalList);
this.loading = false;
});
}
} catch (Exception e) {
if (this.client != null) {
this.client.execute(() -> {
this.errorMessage = e.getMessage();
this.loading = false;
});
}
}
});
}
@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 = 50;
context.drawCenteredTextWithShadow(this.textRenderer, this.title, centerX, 20, 0xFFFFFF);
context.drawCenteredTextWithShadow(this.textRenderer, Text.literal("Карта: " + cardTitle), centerX, 35, 0xBFBFBF);
if (loading) {
context.drawCenteredTextWithShadow(this.textRenderer, Text.literal("Загрузка транзакций..."), centerX, this.height / 2 - 10, 0xCCCCCC);
} else if (!errorMessage.isEmpty()) {
context.drawCenteredTextWithShadow(this.textRenderer, Text.literal("Ошибка: " + errorMessage), centerX, this.height / 2 - 10, 0xFF5555);
} else if (transactions.isEmpty()) {
context.drawCenteredTextWithShadow(this.textRenderer, Text.literal("Транзакций не найдено"), centerX, this.height / 2 - 10, 0xCCCCCC);
} else {
int y = startY + 15;
for (int i = 0; i < Math.min(6, transactions.size()); i++) {
LocalTransaction tx = transactions.get(i);
String amountText = tx.amount() + " АР";
String dateText = tx.createdAt();
if (dateText.length() > 19) {
dateText = dateText.substring(0, 19).replace("T", " ");
}
String commentText = tx.comment().isEmpty() ? "" : " (" + tx.comment() + ")";
String line = String.format("%s -> %s | %s%s", dateText, tx.receiver(), amountText, commentText);
int color = tx.status().equalsIgnoreCase("SUCCESS") ? 0x55FF55 : 0xFF5555;
context.drawCenteredTextWithShadow(this.textRenderer, Text.literal(line), centerX, y, color);
y += 18;
}
}
}
}
@@ -0,0 +1,368 @@
package git.yawaflua.tech.spmega.client.ui.service;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.mojang.authlib.exceptions.AuthenticationException;
import com.mojang.authlib.minecraft.MinecraftSessionService;
import git.yawaflua.tech.spmega.ModConfig;
import git.yawaflua.tech.spmega.SPMega;
import net.minecraft.client.MinecraftClient;
import net.minecraft.client.session.Session;
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.util.ArrayList;
import java.util.List;
import java.util.UUID;
public class BackendAuthenticator {
public static boolean authenticate(MinecraftClient client) {
Session session = client.getSession();
if (session == null || session.getUuidOrNull() == null) {
System.err.println("Cannot authenticate: Client has no valid session.");
return false;
}
MinecraftSessionService sessionService = client.getApiServices().sessionService();
try {
var serverId = sendStartSessionRequestToBackend(session.getUsername(), session.getUuidOrNull());
System.out.println("[SPMEGA] Trying to auth in mojang with serverId: " + serverId);
sessionService.joinServer(
session.getUuidOrNull(),
session.getAccessToken(),
serverId
);
System.out.println("[SPMEGA] Sending session submitter to backend");
return sendAuthRequestToBackend(session.getUuidOrNull(), serverId);
} catch (AuthenticationException e) {
System.err.println("I cant auth by Mojang: " + e.getMessage());
System.err.println("Please check your credentials and try again.");
return false;
} catch (Exception e) {
System.err.println("Failed to authenticate with backend: " + e.getMessage());
return false;
}
}
private static String sendStartSessionRequestToBackend(String username, UUID uuid) throws IOException, InterruptedException {
ModConfig config = SPMega.getConfig();
String apiDomain = (config != null) ? config.apiDomain() : ModConfig.DEFAULT_API_DOMAIN;
if (apiDomain == null || apiDomain.isBlank()) {
apiDomain = ModConfig.DEFAULT_API_DOMAIN;
}
if (apiDomain.endsWith("/")) {
apiDomain = apiDomain.substring(0, apiDomain.length() - 1);
}
JsonObject jsonPayload = new JsonObject();
jsonPayload.addProperty("userName", username);
jsonPayload.addProperty("userUUID", uuid.toString());
String requestBody = jsonPayload.toString();
String url = apiDomain + "/api/v1/auth/start";
HttpClient httpClient = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder(URI.create(url))
.header("Content-Type", "application/json")
.header("Accept", "application/json")
.method("POST", HttpRequest.BodyPublishers.ofString(requestBody))
.build();
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() != 200) {
System.err.println("[SPMEGA] Server returned status code " + response.statusCode() + " on start: " + response.body());
throw new IOException("Server returned status code " + response.statusCode() + " on start: " + response.body());
}
JsonObject json = JsonParser.parseString(response.body()).getAsJsonObject();
if (json.has("sessionId")) {
return json.get("sessionId").getAsString();
} else {
System.err.println("[SPMEGA] Invalid response from start endpoint: " + response.body());
throw new IOException("Invalid response from start endpoint: " + response.body());
}
}
private static boolean sendAuthRequestToBackend(UUID uuid, String serverId) throws IOException, InterruptedException {
ModConfig config = SPMega.getConfig();
String apiDomain = (config != null) ? config.apiDomain() : ModConfig.DEFAULT_API_DOMAIN;
if (apiDomain == null || apiDomain.isBlank()) {
apiDomain = ModConfig.DEFAULT_API_DOMAIN;
}
if (apiDomain.endsWith("/")) {
apiDomain = apiDomain.substring(0, apiDomain.length() - 1);
}
JsonObject jsonPayload = new JsonObject();
jsonPayload.addProperty("userUUID", uuid.toString());
jsonPayload.addProperty("sessionId", serverId);
String requestBody = jsonPayload.toString();
String url = apiDomain + "/api/v1/auth/validate";
HttpClient httpClient = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder(URI.create(url))
.header("Content-Type", "application/json")
.header("Accept", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(requestBody))
.build();
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() != 200) {
System.err.println("[SPMEGA] Server returned status code " + response.statusCode() + " on validate: " + response.body());
throw new IOException("Server returned status code " + response.statusCode() + " on validate: " + response.body());
}
JsonObject json = JsonParser.parseString(response.body()).getAsJsonObject();
if (json.has("token")) {
String token = json.get("token").getAsString();
if (config != null) {
ModConfig updated = new ModConfig(
config.apiDomain(),
token,
config.allowBackend(),
config.signQuickPayEnabled(),
config.gpsEnabled(),
config.gpsPosition()
);
SPMega.setConfig(updated);
System.out.println("[SPMEGA] Backend auth successful, saved token.");
return true;
} else {
System.err.println("[SPMEGA] Config is null, cannot save token.");
throw new IOException("Config is null, cannot save token.");
}
} else {
System.err.println("[SPMEGA] Invalid response from validate endpoint: " + response.body());
throw new IOException("Invalid response from validate endpoint: " + response.body());
}
}
public static void sendCardToBackend(String cardId, String cardToken) {
ModConfig config = SPMega.getConfig();
if (config == null || !config.allowBackend()) {
return;
}
String apiDomain = config.apiDomain();
if (apiDomain == null || apiDomain.isBlank()) {
apiDomain = ModConfig.DEFAULT_API_DOMAIN;
}
if (apiDomain.endsWith("/")) {
apiDomain = apiDomain.substring(0, apiDomain.length() - 1);
}
String url = apiDomain + "/api/v1/auth/cards";
JsonObject jsonPayload = new JsonObject();
jsonPayload.addProperty("id", cardId);
jsonPayload.addProperty("token", cardToken);
String requestBody = jsonPayload.toString();
HttpClient httpClient = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder(URI.create(url))
.header("Content-Type", "application/json")
.header("Accept", "application/json")
.header("Authorization", "Bearer " + config.apiToken())
.PUT(HttpRequest.BodyPublishers.ofString(requestBody))
.build();
httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofString())
.thenAccept(response -> {
if (response.statusCode() != 200 && response.statusCode() != 204) {
System.err.println("[SPMEGA] Failed to send card to backend: status code "
+ response.statusCode() + ", body: " + response.body());
} else {
System.out.println("[SPMEGA] Card successfully synchronized with backend.");
}
})
.exceptionally(throwable -> {
System.err.println("[SPMEGA] Error sending card to backend: " + throwable.getMessage());
return null;
});
}
public static List<CardCredentials> fetchCardsFromBackend() throws IOException, InterruptedException {
ModConfig config = SPMega.getConfig();
if (config == null || !config.allowBackend()) {
return List.of();
}
String apiDomain = config.apiDomain();
if (apiDomain == null || apiDomain.isBlank()) {
apiDomain = ModConfig.DEFAULT_API_DOMAIN;
}
if (apiDomain.endsWith("/")) {
apiDomain = apiDomain.substring(0, apiDomain.length() - 1);
}
String url = apiDomain + "/api/v1/auth/cards";
HttpClient httpClient = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder(URI.create(url))
.header("Accept", "application/json")
.header("Authorization", "Bearer " + config.apiToken())
.GET()
.build();
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() != 200) {
throw new IOException("Server returned status code " + response.statusCode() + " on fetch cards.");
}
com.google.gson.JsonArray cardsArray = JsonParser.parseString(response.body()).getAsJsonArray();
List<CardCredentials> cards = new ArrayList<>();
for (com.google.gson.JsonElement el : cardsArray) {
JsonObject cardJson = el.getAsJsonObject();
String cardId = "";
if (cardJson.has("cardId") && !cardJson.get("cardId").isJsonNull()) {
cardId = cardJson.get("cardId").getAsString();
} else if (cardJson.has("id") && !cardJson.get("id").isJsonNull()) {
cardId = cardJson.get("id").getAsString();
}
String cardToken = "";
if (cardJson.has("cardToken") && !cardJson.get("cardToken").isJsonNull()) {
cardToken = cardJson.get("cardToken").getAsString();
} else if (cardJson.has("token") && !cardJson.get("token").isJsonNull()) {
cardToken = cardJson.get("token").getAsString();
}
if (!cardId.isEmpty() && !cardToken.isEmpty()) {
cards.add(new CardCredentials(cardId, cardToken));
}
}
return cards;
}
public static void deleteCardOnBackend(String cardId) {
ModConfig config = SPMega.getConfig();
if (config == null || !config.allowBackend()) {
return;
}
String apiDomain = config.apiDomain();
if (apiDomain == null || apiDomain.isBlank()) {
apiDomain = ModConfig.DEFAULT_API_DOMAIN;
}
if (apiDomain.endsWith("/")) {
apiDomain = apiDomain.substring(0, apiDomain.length() - 1);
}
String url = apiDomain + "/api/v1/auth/cards/" + cardId;
HttpClient httpClient = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder(URI.create(url))
.header("Accept", "application/json")
.header("Authorization", "Bearer " + config.apiToken())
.DELETE()
.build();
httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofString())
.thenAccept(response -> {
if (response.statusCode() != 200 && response.statusCode() != 204) {
System.err.println("[SPMEGA] Failed to delete card from backend: status code "
+ response.statusCode() + ", body: " + response.body());
} else {
System.out.println("[SPMEGA] Card successfully deleted from backend.");
}
})
.exceptionally(throwable -> {
System.err.println("[SPMEGA] Error deleting card from backend: " + throwable.getMessage());
return null;
});
}
public static boolean createTransactionOnBackend(String senderCardId, String receiver, long amount, String comment) throws IOException, InterruptedException {
ModConfig config = SPMega.getConfig();
if (config == null) {
throw new IOException("ModConfig is null");
}
String apiDomain = config.apiDomain();
if (apiDomain == null || apiDomain.isBlank()) {
apiDomain = ModConfig.DEFAULT_API_DOMAIN;
}
if (apiDomain.endsWith("/")) {
apiDomain = apiDomain.substring(0, apiDomain.length() - 1);
}
String url = apiDomain + "/api/v1/transactions";
JsonObject jsonPayload = new JsonObject();
jsonPayload.addProperty("senderCardId", senderCardId);
jsonPayload.addProperty("cardId", senderCardId);
jsonPayload.addProperty("receiver", receiver);
jsonPayload.addProperty("recipient", receiver);
jsonPayload.addProperty("amount", amount);
jsonPayload.addProperty("comment", comment);
String requestBody = jsonPayload.toString();
HttpClient httpClient = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder(URI.create(url))
.header("Content-Type", "application/json")
.header("Accept", "application/json")
.header("Authorization", "Bearer " + config.apiToken())
.PUT(HttpRequest.BodyPublishers.ofString(requestBody))
.build();
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() == 200 || response.statusCode() == 201 || response.statusCode() == 204) {
return true;
} else {
throw new IOException("Server returned status code " + response.statusCode() + ": " + response.body());
}
}
public static List<BankDatabase.LocalTransaction> fetchTransactionsFromBackend(String cardId) throws IOException, InterruptedException {
ModConfig config = SPMega.getConfig();
if (config == null || !config.allowBackend()) {
return List.of();
}
String apiDomain = config.apiDomain();
if (apiDomain == null || apiDomain.isBlank()) {
apiDomain = ModConfig.DEFAULT_API_DOMAIN;
}
if (apiDomain.endsWith("/")) {
apiDomain = apiDomain.substring(0, apiDomain.length() - 1);
}
String url = apiDomain + "/api/v1/transactions?cardId=" + cardId;
HttpClient httpClient = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder(URI.create(url))
.header("Accept", "application/json")
.header("Authorization", "Bearer " + config.apiToken())
.GET()
.build();
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() != 200) {
throw new IOException("Server returned status code " + response.statusCode() + " on fetch transactions.");
}
com.google.gson.JsonArray transactionsArray = JsonParser.parseString(response.body()).getAsJsonArray();
List<BankDatabase.LocalTransaction> list = new ArrayList<>();
for (com.google.gson.JsonElement el : transactionsArray) {
JsonObject json = el.getAsJsonObject();
String receiver = json.has("receiver") ? json.get("receiver").getAsString() : (json.has("recipient") ? json.get("recipient").getAsString() : "unknown");
long amount = json.has("amount") ? json.get("amount").getAsLong() : 0L;
String comment = json.has("comment") && !json.get("comment").isJsonNull() ? json.get("comment").getAsString() : "";
String status = json.has("status") ? json.get("status").getAsString() : "SUCCESS";
String createdAt = json.has("createdAt") ? json.get("createdAt").getAsString() : (json.has("created_at") ? json.get("created_at").getAsString() : "");
list.add(new BankDatabase.LocalTransaction(receiver, amount, comment, status, createdAt));
}
return list;
}
}
@@ -166,6 +166,31 @@ public final class BankDatabase {
}
}
public synchronized List<LocalTransaction> loadTransferHistory(String cardId) {
String sql = "SELECT receiver, amount, comment, status, created_at FROM transfer_history WHERE sender_card_id = ? ORDER BY id DESC";
List<LocalTransaction> result = new ArrayList<>();
try (Connection connection = open(); PreparedStatement statement = connection.prepareStatement(sql)) {
statement.setString(1, cardId);
try (ResultSet rs = statement.executeQuery()) {
while (rs.next()) {
result.add(new LocalTransaction(
rs.getString("receiver"),
rs.getLong("amount"),
rs.getString("comment"),
rs.getString("status"),
rs.getString("created_at")
));
}
}
return result;
} catch (SQLException exception) {
throw new RuntimeException("Failed to load transfer history", exception);
}
}
public record LocalTransaction(String receiver, long amount, String comment, String status, String createdAt) {
}
private Connection open() throws SQLException {
return DriverManager.getConnection(jdbcUrl);
}
@@ -1,5 +1,7 @@
package git.yawaflua.tech.spmega.client.ui.service;
import git.yawaflua.tech.spmega.ModConfig;
import git.yawaflua.tech.spmega.SPMega;
import git.yawaflua.tech.spmega.api.SPWorldsApiClient;
import net.fabricmc.loader.api.FabricLoader;
@@ -92,7 +94,28 @@ public final class BankUiService {
return lastMessage;
}
public synchronized void syncCardsWithBackend(String playerUuid) {
try {
List<CardCredentials> backendCards = BackendAuthenticator.fetchCardsFromBackend();
boolean changed = false;
for (CardCredentials creds : backendCards) {
if (database.getCredentials(creds.cardId()) == null) {
database.upsertCardCredentials(creds.cardId(), creds.cardToken());
refreshCard(creds.cardId(), creds.cardToken(), playerUuid, false, false);
changed = true;
}
}
if (changed) {
reloadCardsFromDb();
}
} catch (Exception e) {
System.err.println("[SPMEGA] Failed to sync cards with backend: " + e.getMessage());
}
}
public synchronized void refreshOnServerJoin(String playerUuid) {
syncCardsWithBackend(playerUuid);
List<StoredCard> storedCards = database.loadCards();
for (StoredCard card : storedCards) {
refreshCard(card.cardId(), card.cardToken(), playerUuid, false, false);
@@ -158,7 +181,12 @@ public final class BankUiService {
return;
}
database.deleteCard(selected.id());
String cardId = selected.id();
database.deleteCard(cardId);
ModConfig config = SPMega.getConfig();
if (config.allowBackend())
BackendAuthenticator.deleteCardOnBackend(cardId);
reloadCardsFromDb();
if (selectedCardIndex >= cards.size()) {
selectedCardIndex = Math.max(0, cards.size() - 1);
@@ -190,21 +218,47 @@ public final class BankUiService {
return false;
}
git.yawaflua.tech.spmega.ModConfig config = git.yawaflua.tech.spmega.SPMega.getConfig();
boolean useBackend = config != null && config.allowBackend();
try {
SPWorldsApiClient.TransactionResult result = apiClient.createTransaction(
toApiAuth(credentials),
draft.recipient(),
draft.amount(),
draft.comment()
);
long newBalance = 0;
if (useBackend) {
BackendAuthenticator.createTransactionOnBackend(
draft.senderCardId(),
draft.recipient(),
draft.amount(),
draft.comment()
);
try {
refreshCard(credentials.cardId(), credentials.cardToken(), "", false, false);
StoredCard updatedCard = database.loadCards().stream()
.filter(c -> c.cardId().equals(credentials.cardId()))
.findFirst()
.orElse(null);
if (updatedCard != null) {
newBalance = updatedCard.balance();
}
} catch (Exception e) {
System.err.println("[SPMEGA] Failed to refresh card balance after transaction: " + e.getMessage());
}
} else {
SPWorldsApiClient.TransactionResult result = apiClient.createTransaction(
toApiAuth(credentials),
draft.recipient(),
draft.amount(),
draft.comment()
);
newBalance = result.balance();
database.updateCardBalance(credentials.cardId(), newBalance);
}
database.updateCardBalance(credentials.cardId(), result.balance());
database.insertTransferHistory(
credentials.cardId(),
draft.recipient(),
draft.amount(),
draft.comment(),
result.balance(),
newBalance,
"SUCCESS"
);
@@ -264,6 +318,13 @@ public final class BankUiService {
return true;
}
}
git.yawaflua.tech.spmega.ModConfig config = git.yawaflua.tech.spmega.SPMega.getConfig();
if (config.allowBackend())
CompletableFuture.supplyAsync(() -> {
BackendAuthenticator.sendCardToBackend(cardId, cardToken);
return true;
}
);
lastMessage = "";
return true;
@@ -300,4 +361,8 @@ public final class BankUiService {
}
return database.getCredentials(selected.id());
}
public BankDatabase getDatabase() {
return database;
}
}
@@ -14,6 +14,22 @@
"message.spmega.qr.error": "Failed to scan QR from screen",
"message.spmega.qr.hover_tip": "Click to open link",
"message.spmega.qr.found_link": "Found link: %s",
"message.spmega.qr.failed_open": "Failed to open link"
"message.spmega.qr.failed_open": "Failed to open link",
"key.spmega.toggle_gps": "Toggle Nether GPS",
"title.spmega.config": "SPMega Settings",
"category.spmega.general": "General Settings",
"option.spmega.api_domain": "API Domain",
"option.spmega.api_token": "API Token",
"option.spmega.allow_backend": "Allow saving data, transactions and other sensitive information to the backend",
"option.spmega.sign_quick_pay": "Quick Pay by Signs",
"option.spmega.gps_position": "Nether GPS position",
"option.spmega.gps_position.top_left": "Top-Left",
"option.spmega.gps_position.top_right": "Top-Right",
"option.spmega.gps_position.bottom_left": "Bottom-Left",
"option.spmega.gps_position.bottom_right": "Bottom-Right",
"option.spmega.gps_position.bottom_center": "Bottom-center",
"option.spmega.gps_position.top_center": "Top-center"
}
@@ -0,0 +1,33 @@
{
"category.spmega": "SPMega",
"key.spmega.open_menu": "Открыть меню SPMega",
"key.spmega.scan_qr": "Сканировать QR-код с экрана",
"key.spmega.toggle_gps": "Вкл/Выкл GPS Ада",
"button.spmega.scan_qr": "Сканировать QR",
"button.spmega.qr.cancel": "Отмена",
"button.spmega.qr.open_link": "Открыть ссылку",
"screen.spmega.qr.confirm_title": "Подтверждение ссылки",
"screen.spmega.qr.accept_link": "Открыть эту ссылку?",
"message.spmega.qr.capture_failed": "Не удалось сделать снимок экрана Minecraft",
"message.spmega.qr.not_found": "QR-код на экране не найден",
"message.spmega.qr.invalid_url": "QR-код не содержит валидный http/https URL",
"message.spmega.qr.opened": "Открыто: %s",
"message.spmega.qr.error": "Не удалось отсканировать QR-код с экрана",
"message.spmega.qr.hover_tip": "Нажмите, чтобы открыть ссылку",
"message.spmega.qr.found_link": "Найдена ссылка: %s",
"message.spmega.qr.failed_open": "Не удалось открыть ссылку",
"title.spmega.config": "Настройки SPMega",
"category.spmega.general": "Основные настройки",
"option.spmega.api_domain": "Домен API",
"option.spmega.api_token": "Токен API",
"option.spmega.allow_backend": "Разрешить сохранение данных, транзакций и другой получаемой информации на сервере",
"option.spmega.sign_quick_pay": "Быстрая оплата по табличкам",
"option.spmega.gps_position": "Положение GPS Ада",
"option.spmega.gps_position.top_left": "Слева вверху",
"option.spmega.gps_position.top_right": "Справа вверху",
"option.spmega.gps_position.bottom_left": "Слева внизу",
"option.spmega.gps_position.bottom_center": "Снизу по-центру",
"option.spmega.gps_position.top_center": "Сверху по-центру",
"option.spmega.gps_position.bottom_right": "Снизу справа"
}
@@ -42,19 +42,39 @@ public final class ConfigManager {
shouldSave = true;
}
boolean allowAccess = readBoolean(properties, "allow.backend", defaults.allowBackend());
String rawAllowAccess = properties.getProperty("sign.quickPay.enabled");
if (rawAllowAccess == null || !Boolean.toString(allowAccess).equalsIgnoreCase(rawAllowAccess.trim())) {
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);
boolean gpsEnabled = readBoolean(properties, "gps.enabled", defaults.gpsEnabled());
String rawGps = properties.getProperty("gps.enabled");
if (rawGps == null || !Boolean.toString(gpsEnabled).equalsIgnoreCase(rawGps.trim())) {
shouldSave = true;
}
GpsHudPosition gpsPosition = readEnum(properties, "gps.position", GpsHudPosition.class, defaults.gpsPosition());
String rawPosition = properties.getProperty("gps.position");
if (rawPosition == null || !gpsPosition.name().equalsIgnoreCase(rawPosition.trim())) {
shouldSave = true;
}
ModConfig config = new ModConfig(apiDomain, apiToken, allowAccess, signQuickPayEnabled, gpsEnabled, gpsPosition);
if (shouldSave) {
save(configPath, config);
}
return config;
}
private static String readString(Properties properties, String key, String fallback) {
@@ -97,11 +117,18 @@ public final class ConfigManager {
return fallback;
}
public static void save(ModConfig config) {
Path configPath = FabricLoader.getInstance().getConfigDir().resolve(FILE_NAME);
save(configPath, config);
}
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()));
properties.setProperty("gps.enabled", Boolean.toString(config.gpsEnabled()));
properties.setProperty("gps.position", config.gpsPosition().name());
try {
Files.createDirectories(configPath.getParent());
@@ -112,5 +139,17 @@ public final class ConfigManager {
throw new RuntimeException("Failed to save config: " + configPath, exception);
}
}
private static <E extends Enum<E>> E readEnum(Properties properties, String key, Class<E> enumClass, E fallback) {
String value = properties.getProperty(key);
if (value == null || value.trim().isEmpty()) {
return fallback;
}
try {
return Enum.valueOf(enumClass, value.trim().toUpperCase());
} catch (IllegalArgumentException exception) {
return fallback;
}
}
}
@@ -0,0 +1,21 @@
package git.yawaflua.tech.spmega;
public enum GpsHudPosition {
TOP_LEFT("Слева вверху"),
TOP_RIGHT("Справа вверху"),
BOTTOM_LEFT("Слева внизу"),
BOTTOM_RIGHT("Справа внизу"),
TOP_CENTER("Сверху по-центру"),
BOTTOM_CENTER("Снизу по-центру");
private final String displayName;
GpsHudPosition(String displayName) {
this.displayName = displayName;
}
public String getDisplayName() {
return displayName;
}
}
@@ -1,16 +1,24 @@
package git.yawaflua.tech.spmega;
public record ModConfig(String apiDomain, String apiToken, boolean signQuickPayEnabled) {
public static final String DEFAULT_API_DOMAIN = "https://spmega-api.yawaflua.tech";
public record ModConfig(String apiDomain, String apiToken, boolean allowBackend, boolean signQuickPayEnabled,
boolean gpsEnabled, GpsHudPosition gpsPosition) {
public static final String DEFAULT_API_DOMAIN = "http://localhost:5129";
public static final boolean ALLOW_BACKEND = false;
public static final String DEFAULT_API_TOKEN = "ulBKE9MWEtIGiPAhXV69I28W9BRiSrV3";
public static final boolean DEFAULT_SIGN_QUICK_PAY_ENABLED = true;
public static final boolean DEFAULT_GPS_ENABLED = true;
public static final GpsHudPosition DEFAULT_GPS_POSITION = GpsHudPosition.TOP_LEFT;
public static ModConfig createDefault() {
return new ModConfig(
DEFAULT_API_DOMAIN,
DEFAULT_API_TOKEN,
DEFAULT_SIGN_QUICK_PAY_ENABLED
ALLOW_BACKEND,
DEFAULT_SIGN_QUICK_PAY_ENABLED,
DEFAULT_GPS_ENABLED,
DEFAULT_GPS_POSITION
);
}
}
@@ -9,6 +9,11 @@ public class SPMega implements ModInitializer {
return config;
}
public static void setConfig(ModConfig newConfig) {
config = newConfig;
ConfigManager.save(newConfig);
}
@Override
public void onInitialize() {
config = ConfigManager.loadOrCreate();
+4
View File
@@ -8,6 +8,7 @@
"Dmitri 'yawaflua' Shimanski aka DOLBAYEB"
],
"contact": {
},
"license": "CC BY-NC-ND 4.0",
"icon": "icon.png",
@@ -19,6 +20,9 @@
"client": [
"git.yawaflua.tech.spmega.client.SPMegaClient"
],
"modmenu": [
"git.yawaflua.tech.spmega.client.ModMenuIntegration"
],
"main": [
"git.yawaflua.tech.spmega.SPMega"
]