Add backend structure with Docker support and initial configuration files
Java CI / build (push) Successful in 1m56s
Java CI / build (push) Successful in 1m56s
Signed-off-by: Dmitrii <computer@yawaflua.tech> Took 4 hours 25 minutes
This commit is contained in:
@@ -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
|
||||
@@ -1,4 +0,0 @@
|
||||
SpMega.Backend/src/*
|
||||
SpMega.Backend/obj/*
|
||||
SpMega.Backend/appsettings.json
|
||||
SpMega.Backend/SpMega.Backend.http
|
||||
@@ -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"]
|
||||
@@ -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": "*"
|
||||
}
|
||||
@@ -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
|
||||
@@ -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();
|
||||
|
||||
@@ -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"
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user