diff --git a/README.md b/README.md index 325482d..74dc959 100644 --- a/README.md +++ b/README.md @@ -32,12 +32,63 @@ SPMega - клиентский Fabric-мод с банковым UI для раб `Вы не владелец карты. Часть функций может быть ограничена.` ###### позже это станет ограничением для синхронизации карт/истории с облаком -## Сборка (PowerShell) +## Поддерживаемые версии + +- Minecraft 1.21.1 (Java 21) +- Minecraft 1.21.11 (Java 21) +- Minecraft 26.1.x (сборка на 26.1.2, Java 25) +- Minecraft 26.2.x (сборка на 26.2, Java 25) + +Версии собираются из общего дерева исходников через Stonecutter. Зависимости +для каждой версии находятся в `stonecutter.properties.toml`, а условные участки +исходников помечены директивами `/*? ... */`. + +## Сборка + +Все поддерживаемые версии: + +```bash +./gradlew buildAndCollect +``` + +Готовые JAR-файлы появятся в `build/libs/<версия-мода>/`. + +Одна версия: + +```bash +./gradlew :1.21.1:build +./gradlew :26.2.x:build +``` + +PowerShell: ```powershell -.\gradlew.bat build +.\gradlew.bat buildAndCollect ``` +## Публикация на Modrinth + +Один универсальный JAR для этих версий использовать нельзя: сборки 1.21.x и +26.x имеют несовместимые Minecraft/Fabric API. Вместо ручной загрузки четырёх +файлов Gradle создаёт отдельную запись Modrinth для каждой версии: + +```bash +export MODRINTH_TOKEN="ваш-токен" +export MODRINTH_PROJECT_ID="id-или-slug-проекта" +./gradlew modrinth +``` + +В PowerShell: + +```powershell +$env:MODRINTH_TOKEN="ваш-токен" +$env:MODRINTH_PROJECT_ID="id-или-slug-проекта" +.\gradlew.bat modrinth +``` + +Тип релиза задаётся свойством `modrinth_version_type` в `gradle.properties`: +`release`, `beta` или `alpha`. + ## Дорожная карта - [ ] Синхронизация карт/истории с облаком diff --git a/backend/SpMega.Backend/Authetication/JwtAuthHandler.cs b/backend/SpMega.Backend/Authetication/JwtAuthHandler.cs index 8cb1ac4..9ccd89f 100644 --- a/backend/SpMega.Backend/Authetication/JwtAuthHandler.cs +++ b/backend/SpMega.Backend/Authetication/JwtAuthHandler.cs @@ -3,7 +3,9 @@ using System.Text.Encodings.Web; using Microsoft.AspNetCore.Authentication; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Options; +using Microsoft.IdentityModel.Tokens; using SpMega.Backend.Persistent.Database; +using SpMega.Backend.Services; namespace SpMega.Backend.Authetication; @@ -11,28 +13,39 @@ public class JwtAuthHandler( IOptionsMonitor options, ILoggerFactory logger, UrlEncoder encoder, - AppDbContext dbContext) : AuthenticationHandler(options, logger, encoder) + AppDbContext dbContext, + TokenService tokenService) : AuthenticationHandler(options, logger, encoder) { - protected override async Task HandleAuthenticateAsync() { - if (!Request.Headers.ContainsKey("Authorization") || !Request.Headers.Authorization.ToString().StartsWith("Bearer ")) - return AuthenticateResult.NoResult(); - - var token = Request.Headers.Authorization.ToString().Replace("Bearer ", ""); - - if (string.IsNullOrEmpty(token)) + var authorization = Request.Headers.Authorization.ToString(); + if (string.IsNullOrWhiteSpace(authorization)) { - return AuthenticateResult.Fail("Invalid authorization header format"); + return AuthenticateResult.NoResult(); + } + + const string bearerPrefix = "Bearer "; + if (!authorization.StartsWith(bearerPrefix, StringComparison.OrdinalIgnoreCase)) + { + return AuthenticateResult.Fail("Invalid authorization header format."); + } + + var token = authorization[bearerPrefix.Length..].Trim(); + if (string.IsNullOrWhiteSpace(token)) + { + return AuthenticateResult.Fail("Invalid authorization header format."); } try { - var user = await dbContext.Users.FirstOrDefaultAsync(k => k.Token == token); + var validatedToken = tokenService.ValidateAccessToken(token); + var user = await dbContext.Users.FirstOrDefaultAsync( + candidate => candidate.Id == validatedToken.UserId && !candidate.IsDeleted, + Context.RequestAborted); - if (user == null) + if (user == null || !string.Equals(user.Username, validatedToken.UserName, StringComparison.Ordinal)) { - return AuthenticateResult.Fail("Invalid or expired token"); + return AuthenticateResult.Fail("Invalid token subject."); } var claims = new List @@ -40,25 +53,39 @@ public class JwtAuthHandler( new(ClaimTypes.NameIdentifier, user.Id.ToString()), new(ClaimTypes.Name, user.Username), new("UserId", user.Id.ToString()), - new("Token", token), new(ClaimTypes.Role, "default") }; - var claimsIdentity = new ClaimsIdentity(claims, Scheme.Name); - var claimsPrincipal = new ClaimsPrincipal(claimsIdentity); - + var principal = new ClaimsPrincipal(new ClaimsIdentity(claims, Scheme.Name)); Context.Items["User"] = user; Context.Items["@me"] = user; Context.Items["user"] = user; + Context.Items[TokenService.HttpContextItemKey] = validatedToken; - var ticket = new AuthenticationTicket(claimsPrincipal, Scheme.Name); - - return AuthenticateResult.Success(ticket); + return AuthenticateResult.Success(new AuthenticationTicket(principal, Scheme.Name)); } - catch (Exception ex) + catch (SecurityTokenException exception) { - Logger.LogError(ex, "Error during JWT authentication"); - return AuthenticateResult.Fail("Authentication error occurred"); + Logger.LogWarning("JWT authentication rejected: {Reason}", exception.Message); + return AuthenticateResult.Fail("Invalid or expired token."); + } + catch (Exception exception) + { + Logger.LogError(exception, "Error during JWT authentication"); + return AuthenticateResult.Fail("Authentication error occurred."); } } + + protected override Task HandleChallengeAsync(AuthenticationProperties properties) + { + Response.StatusCode = StatusCodes.Status401Unauthorized; + Response.ContentType = "application/json"; + Response.Headers.WWWAuthenticate = $"{Scheme.Name} error=\"invalid_token\""; + Response.Headers["X-SPMega-Reauthenticate"] = "true"; + return Response.WriteAsJsonAsync(new + { + code = "reauthentication_required", + message = "The access token is missing, invalid, expired, or older than two days." + }); + } } diff --git a/backend/SpMega.Backend/Controllers/v1/AuthController.cs b/backend/SpMega.Backend/Controllers/v1/AuthController.cs index 322ca02..a9eb6be 100644 --- a/backend/SpMega.Backend/Controllers/v1/AuthController.cs +++ b/backend/SpMega.Backend/Controllers/v1/AuthController.cs @@ -128,6 +128,35 @@ public class AuthController(AppDbContext dbContext, TokenService tokenService, I } + [HttpPost("refresh")] + [Authorize] + public async Task RefreshAccessTokenAsync() + { + if (HttpContext.Items[TokenService.HttpContextItemKey] is not ValidatedAccessToken accessToken || + HttpContext.Items["@me"] is not User user) + { + return Unauthorized(new { code = "reauthentication_required" }); + } + + var now = DateTimeOffset.UtcNow; + if (accessToken.ExpiresAt - now > TokenService.RefreshThreshold) + { + return Ok(new { refreshed = false, expiresAt = accessToken.ExpiresAt }); + } + + if (accessToken.AuthenticatedAt.Add(TokenService.MaximumAuthenticationAge) - now <= TokenService.RefreshThreshold) + { + return Unauthorized(new { code = "reauthentication_required" }); + } + + var token = tokenService.GenerateAccessToken(user.Username, user.Id, accessToken.AuthenticatedAt); + user.Token = token; + user.UpdatedAt = DateTime.UtcNow; + await dbContext.SaveChangesAsync(); + + return Ok(new { refreshed = true, token }); + } + [HttpPut("cards")] [Authorize] public async Task AddCardAsync([FromBody] AddCardBody body) @@ -145,11 +174,6 @@ public class AuthController(AppDbContext dbContext, TokenService tokenService, I throw new Exception("Its not ur card"); } - var balanceResp = await SendRequest("/public/card", new("Bearer", Base64BearerToken)); - var balance = (int?)JsonNode.Parse(balanceResp)?["balance"]; - - - var card = me.cards.First(k => k.id == body.id); var existingCard = user.Cards.FirstOrDefault(k => k.Id.ToString() == card.id); if (existingCard == null) @@ -157,7 +181,6 @@ public class AuthController(AppDbContext dbContext, TokenService tokenService, I { Id = Guid.Parse(card.id), Name = card.name, - Balance = balance ?? -1, SpworldsID = card.number, Token = Base64BearerToken, CreatedAt = DateTime.UtcNow, @@ -168,7 +191,6 @@ public class AuthController(AppDbContext dbContext, TokenService tokenService, I existingCard.Name = card.name; existingCard.SpworldsID = card.number; existingCard.Token = Base64BearerToken; - existingCard.Balance = balance ?? -1; existingCard.UpdatedAt = DateTime.UtcNow; } @@ -236,4 +258,3 @@ public class AuthController(AppDbContext dbContext, TokenService tokenService, I } public record AddCardBody(string id, string token); - diff --git a/backend/SpMega.Backend/Controllers/v1/TransactionsController.cs b/backend/SpMega.Backend/Controllers/v1/TransactionsController.cs index 99778c7..c50d951 100644 --- a/backend/SpMega.Backend/Controllers/v1/TransactionsController.cs +++ b/backend/SpMega.Backend/Controllers/v1/TransactionsController.cs @@ -65,10 +65,6 @@ public class TransactionsController(AppDbContext context, ILogger maximumExpiration) + { + expiration = maximumExpiration; + } + + if (expiration <= now) + { + throw new SecurityTokenExpiredException("The original authentication is older than two days."); + } + var claims = new List { new(JwtRegisteredClaimNames.Name, userName), - new (JwtRegisteredClaimNames.UniqueName, uuid.ToString()), - new(JwtRegisteredClaimNames.AuthTime, DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString()), + new(JwtRegisteredClaimNames.UniqueName, uuid.ToString()), + new(JwtRegisteredClaimNames.AuthTime, authenticatedAt.ToUnixTimeSeconds().ToString(CultureInfo.InvariantCulture)), new(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()) }; - var tokenHandler = new JwtSecurityTokenHandler(); - var key = Encoding.ASCII.GetBytes(_secret); - + var tokenHandler = new JwtSecurityTokenHandler { MapInboundClaims = false }; var tokenDescriptor = new SecurityTokenDescriptor { Subject = new ClaimsIdentity(claims), - Expires = DateTime.UtcNow.AddHours(AccessTokenExpirationMinutes), + Expires = expiration.UtcDateTime, Issuer = _issuer, - SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature), - IssuedAt = DateTime.UtcNow + SigningCredentials = new SigningCredentials( + new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_secret)), + SecurityAlgorithms.HmacSha256), + IssuedAt = now.UtcDateTime }; - var token = tokenHandler.CreateToken(tokenDescriptor); - - return tokenHandler.WriteToken(token); + return tokenHandler.WriteToken(tokenHandler.CreateToken(tokenDescriptor)); } -} \ No newline at end of file + + public ValidatedAccessToken ValidateAccessToken(string token) + { + var tokenHandler = new JwtSecurityTokenHandler { MapInboundClaims = false }; + var principal = tokenHandler.ValidateToken(token, new TokenValidationParameters + { + ValidateIssuer = true, + ValidIssuer = _issuer, + ValidateAudience = false, + ValidateIssuerSigningKey = true, + IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_secret)), + ValidateLifetime = true, + RequireExpirationTime = true, + RequireSignedTokens = true, + ClockSkew = ClockSkew, + ValidAlgorithms = [SecurityAlgorithms.HmacSha256] + }, out var validatedToken); + + if (validatedToken is not JwtSecurityToken jwtToken || + !string.Equals(jwtToken.Header.Alg, SecurityAlgorithms.HmacSha256, StringComparison.Ordinal)) + { + throw new SecurityTokenValidationException("Unsupported JWT signing algorithm."); + } + + var userName = principal.FindFirst(JwtRegisteredClaimNames.Name)?.Value; + var uniqueName = principal.FindFirst(JwtRegisteredClaimNames.UniqueName)?.Value; + var authTimeValue = principal.FindFirst(JwtRegisteredClaimNames.AuthTime)?.Value; + var expirationValue = principal.FindFirst(JwtRegisteredClaimNames.Exp)?.Value; + + if (string.IsNullOrWhiteSpace(userName) || + !Guid.TryParse(uniqueName, out var userId) || + !long.TryParse(authTimeValue, NumberStyles.None, CultureInfo.InvariantCulture, out var authTimeSeconds) || + !long.TryParse(expirationValue, NumberStyles.None, CultureInfo.InvariantCulture, out var expirationSeconds)) + { + throw new SecurityTokenValidationException("Required JWT claims are missing or invalid."); + } + + DateTimeOffset authenticatedAt; + DateTimeOffset expiresAt; + try + { + authenticatedAt = DateTimeOffset.FromUnixTimeSeconds(authTimeSeconds); + expiresAt = DateTimeOffset.FromUnixTimeSeconds(expirationSeconds); + } + catch (ArgumentOutOfRangeException exception) + { + throw new SecurityTokenValidationException("JWT timestamps are invalid.", exception); + } + + var now = DateTimeOffset.UtcNow; + if (authenticatedAt > now.Add(ClockSkew) || now - authenticatedAt > MaximumAuthenticationAge) + { + throw new SecurityTokenExpiredException("Reauthentication is required."); + } + + return new ValidatedAccessToken(userId, userName, authenticatedAt, expiresAt); + } +} diff --git a/build.gradle b/build.gradle deleted file mode 100644 index 1d6a84c..0000000 --- a/build.gradle +++ /dev/null @@ -1,113 +0,0 @@ -plugins { - id 'fabric-loom' version '1.14.10' - id 'maven-publish' -} - -version = project.mod_version -group = project.maven_group - -base { - archivesName = project.archives_base_name -} - -loom { - splitEnvironmentSourceSets() - - mods { - "spmega" { - sourceSet sourceSets.main - sourceSet sourceSets.client - } - } -} - -fabricApi { - configureDataGeneration { - client = true - } -} - -repositories { - maven { url "https://maven.shedaniel.me/" } - maven { url "https://maven.terraformersmc.com/" } -} - -dependencies { - minecraft "com.mojang:minecraft:${project.minecraft_version}" - mappings "net.fabricmc:yarn:${project.yarn_mappings}:v2" - modImplementation "net.fabricmc:fabric-loader:${project.loader_version}" - - modImplementation "net.fabricmc.fabric-api:fabric-api:${project.fabric_version}" - - modImplementation("me.shedaniel.cloth:cloth-config-fabric:${project.cloth_config_version}") { - exclude(group: "net.fabricmc.fabric-api") - } - - modCompileOnly "com.terraformersmc:modmenu:11.0.3" - - include(implementation("org.xerial:sqlite-jdbc:3.46.1.3")) - include(clientImplementation("com.google.zxing:core:${project.zxing_version}")) - include(clientImplementation("com.google.zxing:javase:${project.zxing_version}")) -} - -processResources { - inputs.property "version", project.version - inputs.property "minecraft_version", project.minecraft_version - inputs.property "loader_version", project.loader_version - inputs.property "cloth_config_version", project.cloth_config_version - filteringCharset "UTF-8" - - filesMatching("fabric.mod.json") { - expand "version": project.version, - "minecraft_version": project.minecraft_version, - "loader_version": project.loader_version, - "cloth_config_version": project.cloth_config_version - } -} - -def targetJavaVersion = 21 -tasks.withType(JavaCompile).configureEach { - // ensure that the encoding is set to UTF-8, no matter what the system default is - // this fixes some edge cases with special characters not displaying correctly - // see http://yodaconditions.net/blog/fix-for-java-file-encoding-problems-with-gradle.html - // If Javadoc is generated, this must be specified in that task too. - it.options.encoding = "UTF-8" - if (targetJavaVersion >= 10 || JavaVersion.current().isJava10Compatible()) { - it.options.release.set(targetJavaVersion) - } -} - -java { - def javaVersion = JavaVersion.toVersion(targetJavaVersion) - if (JavaVersion.current() < javaVersion) { - toolchain.languageVersion = JavaLanguageVersion.of(targetJavaVersion) - } - // Loom will automatically attach sourcesJar to a RemapSourcesJar task and to the "build" task - // if it is present. - // If you remove this line, sources will not be generated. - withSourcesJar() -} - -jar { - from("LICENSE") { - rename { "${it}_${project.archives_base_name}" } - } -} - -// configure the maven publication -publishing { - publications { - create("mavenJava", MavenPublication) { - artifactId = project.archives_base_name - from components.java - } - } - - // See https://docs.gradle.org/current/userguide/publishing_maven.html for information on how to set up publishing. - repositories { - // Add repositories to publish to here. - // Notice: This block does NOT have the same function as the block in the top level. - // The repositories here will be used for publishing your artifact, not for - // retrieving dependencies. - } -} diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 0000000..0838d21 --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,160 @@ +plugins { + id("dev.kikugie.loom-back-compat") + id("com.modrinth.minotaur") + `maven-publish` +} + +version = "${property("mod_version")}+${sc.current.version}" +group = property("maven_group") as String + +base { + archivesName = property("archives_base_name") as String +} + +val requiredJava = if (sc.current.parsed >= "26.1") JavaVersion.VERSION_25 else JavaVersion.VERSION_21 +val fabricVersion: String = sc.properties["dependencies.fabric_version"] +val clothConfigVersion: String = sc.properties["dependencies.cloth_config_version"] +val modMenuVersion: String = sc.properties["dependencies.modmenu_version"] +val minecraftCompat: String = sc.properties["metadata.minecraft_compat"] +val loaderCompat: String = sc.properties["metadata.loader_compat"] +val modrinthGameVersions = when (sc.current.project) { + "1.21.1" -> listOf("1.21.1") + "1.21.11" -> listOf("1.21.11") + "26.1.x" -> listOf("26.1", "26.1.1", "26.1.2") + "26.2.x" -> listOf("26.2") + else -> error("Missing Modrinth game versions for ${sc.current.project}") +} + +loom { + splitEnvironmentSourceSets() + interfaceInjection { + enableDependencyInterfaceInjection = false + } + + mods { + create("spmega") { + sourceSet(sourceSets.main.get()) + sourceSet(sourceSets["client"]) + } + } +} + +fabricApi { + configureDataGeneration { + client = true + } +} + +repositories { + maven("https://maven.shedaniel.me/") + exclusiveContent { + forRepository { + maven("https://api.modrinth.com/maven") { name = "Modrinth" } + } + filter { includeGroup("maven.modrinth") } + } +} + +dependencies { + minecraft("com.mojang:minecraft:${sc.current.version}") + loomx.applyMojangMappings() + + modImplementation("net.fabricmc:fabric-loader:${property("loader_version")}") + if (sc.current.parsed >= "26.1") { + modImplementation("net.fabricmc.fabric-api:fabric-api:$fabricVersion") + } else { + listOf( + "fabric-api-base", + "fabric-data-generation-api-v1", + "fabric-events-interaction-v0", + "fabric-key-binding-api-v1", + "fabric-lifecycle-events-v1", + "fabric-message-api-v1", + "fabric-networking-api-v1", + "fabric-rendering-v1", + "fabric-screen-api-v1" + ).forEach { module -> + modImplementation(fabricApi.module(module, fabricVersion)) + } + } + + modImplementation("me.shedaniel.cloth:cloth-config-fabric:$clothConfigVersion") { + exclude(group = "net.fabricmc.fabric-api") + } + modCompileOnly("maven.modrinth:mOgUt4GM:$modMenuVersion") + + val sqlite = "org.xerial:sqlite-jdbc:${property("sqlite_version")}" + val zxingCore = "com.google.zxing:core:${property("zxing_version")}" + val zxingJavase = "com.google.zxing:javase:${property("zxing_version")}" + implementation(sqlite) + include(sqlite) + add("clientImplementation", zxingCore) + add("clientImplementation", zxingJavase) + include(zxingCore) + include(zxingJavase) +} + +tasks { + processResources { + val values: Map = mapOf( + "version" to project.version.toString(), + "minecraft_version" to minecraftCompat, + "loader_version" to loaderCompat, + "java_version" to requiredJava.majorVersion, + "cloth_config_version" to clothConfigVersion + ) + values.forEach(inputs::property) + filteringCharset = "UTF-8" + filesMatching("fabric.mod.json") { expand(values) } + } + + withType().configureEach { + options.encoding = "UTF-8" + options.release = requiredJava.majorVersion.toInt() + } + + jar { + from(rootProject.file("LICENSE.txt")) { + rename { "${it}_${project.property("archives_base_name")}" } + } + } + + register("buildAndCollect") { + group = "build" + description = "Builds this Minecraft version and collects its distributable JAR" + from(loomx.modJar.flatMap { it.archiveFile }) + into(rootProject.layout.buildDirectory.dir("libs/${project.property("mod_version")}")) + } +} + +java { + sourceCompatibility = requiredJava + targetCompatibility = requiredJava + toolchain.languageVersion = JavaLanguageVersion.of(requiredJava.majorVersion) + withSourcesJar() +} + +publishing { + publications { + create("mavenJava") { + artifactId = property("archives_base_name") as String + from(components["java"]) + } + } +} + +modrinth { + token.set(providers.environmentVariable("MODRINTH_TOKEN")) + projectId.set(providers.environmentVariable("MODRINTH_PROJECT_ID")) + versionNumber.set(project.version.toString()) + versionName.set("SPMega ${project.version}") + versionType.set(property("modrinth_version_type") as String) + uploadFile.set(loomx.modJar) + gameVersions.addAll(modrinthGameVersions) + loaders.add("fabric") + dependencies { + required.project("fabric-api") + optional.project("cloth-config") + optional.project("modmenu") + } +} diff --git a/gradle.properties b/gradle.properties index 28a3a62..fa54df2 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,16 +1,14 @@ # Done to increase the memory available to gradle. org.gradle.jvmargs=-Xmx1G -# Fabric Properties -# check these on https://modmuss50.me/fabric.html -minecraft_version=1.21.11 -yarn_mappings=1.21.11+build.4 -loader_version=0.18.1 +# Shared Fabric properties. Per-Minecraft-version dependencies are in +# stonecutter.properties.toml. +loader_version=0.19.3 +loomx.loom_version=1.17-SNAPSHOT # Mod Properties -mod_version=0.5-beta +mod_version=0.6-prerelease maven_group=git.yawaflua.tech archives_base_name=SPMega -# Dependencies -# check this on https://modmuss50.me/fabric.html -fabric_version=0.141.1+1.21.11 -cloth_config_version=21.11.153 +modrinth_version_type=beta +# Bundled dependencies +sqlite_version=3.46.1.3 zxing_version=3.5.3 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 23449a2..a351597 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-9.2.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.6.1-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/gradlew b/gradlew old mode 100644 new mode 100755 diff --git a/settings.gradle b/settings.gradle deleted file mode 100644 index f91a4fe..0000000 --- a/settings.gradle +++ /dev/null @@ -1,9 +0,0 @@ -pluginManagement { - repositories { - maven { - name = 'Fabric' - url = 'https://maven.fabricmc.net/' - } - gradlePluginPortal() - } -} diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 0000000..accb0db --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1,26 @@ +pluginManagement { + repositories { + mavenCentral() + gradlePluginPortal() + maven("https://maven.fabricmc.net/") { name = "Fabric" } + maven("https://maven.kikugie.dev/releases") { name = "KikuGie Releases" } + maven("https://maven.kikugie.dev/snapshots") { name = "KikuGie Snapshots" } + } +} + +plugins { + id("dev.kikugie.stonecutter") version "0.9.7" + id("dev.kikugie.loom-back-compat") version "0.4" + id("org.gradle.toolchains.foojay-resolver-convention") version "1.0.0" +} + +stonecutter { + create(rootProject) { + versions("1.21.1", "1.21.11") + version("26.1.x", "26.1.2") + version("26.2.x", "26.2") + vcsVersion = "1.21.11" + } +} + +rootProject.name = "SPMega" diff --git a/src/client/java/git/yawaflua/tech/spmega/client/ChatListener.java b/src/client/java/git/yawaflua/tech/spmega/client/ChatListener.java index 0816166..6fdd4e9 100644 --- a/src/client/java/git/yawaflua/tech/spmega/client/ChatListener.java +++ b/src/client/java/git/yawaflua/tech/spmega/client/ChatListener.java @@ -3,10 +3,9 @@ package git.yawaflua.tech.spmega.client; import git.yawaflua.tech.spmega.client.ui.UiNotifications; import git.yawaflua.tech.spmega.client.ui.service.BankUiService; import net.fabricmc.fabric.api.client.message.v1.ClientReceiveMessageEvents; -import net.minecraft.client.MinecraftClient; -import net.minecraft.text.ClickEvent; -import net.minecraft.text.Text; - +import net.minecraft.client.Minecraft; +import net.minecraft.network.chat.ClickEvent; +import net.minecraft.network.chat.Component; import java.util.ArrayList; import java.util.List; import java.util.regex.Pattern; @@ -38,7 +37,7 @@ public class ChatListener { }); } - private void handleMessage(Text message) { + private void handleMessage(Component message) { if (message == null) { return; } @@ -52,9 +51,9 @@ public class ChatListener { String tokenId = clickValues.get(0); String cardId = clickValues.get(1); - MinecraftClient client = MinecraftClient.getInstance(); + Minecraft client = Minecraft.getInstance(); if (client.player != null) { - String playerUuid = client.player.getUuidAsString(); + String playerUuid = client.player.getStringUUID(); BankUiService.instance().addCardAsync(cardId, tokenId, playerUuid) .thenAccept(msg -> { @@ -68,7 +67,7 @@ public class ChatListener { .exceptionally(exception -> { if (client != null) { client.execute(() -> { - notifications.show(Text.literal("Ошибка добавления карты: " + exception.getMessage())); + notifications.show(Component.literal("Ошибка добавления карты: " + exception.getMessage())); }); } return null; @@ -80,7 +79,7 @@ public class ChatListener { } } - private void collectClickEventValues(Text text, List values) { + private void collectClickEventValues(Component text, List values) { if (text == null) { return; } @@ -98,12 +97,13 @@ public class ChatListener { } } - for (Text sibling : text.getSiblings()) { + for (Component sibling : text.getSiblings()) { collectClickEventValues(sibling, values); } } private String getEventValue(ClickEvent clickEvent) { + /*? if mc_1_21_11 {*/ if (clickEvent instanceof ClickEvent.CopyToClipboard(String value)) { return value; } else if (clickEvent instanceof ClickEvent.RunCommand(String command1)) { @@ -118,5 +118,8 @@ public class ChatListener { return String.valueOf(page); } return ""; + /*?} else {*/ + // return clickEvent.getValue(); + /*?}*/ } } diff --git a/src/client/java/git/yawaflua/tech/spmega/client/ModMenuIntegration.java b/src/client/java/git/yawaflua/tech/spmega/client/ModMenuIntegration.java index 6398c41..f010e09 100644 --- a/src/client/java/git/yawaflua/tech/spmega/client/ModMenuIntegration.java +++ b/src/client/java/git/yawaflua/tech/spmega/client/ModMenuIntegration.java @@ -9,9 +9,9 @@ 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; +import net.minecraft.ChatFormatting; +import net.minecraft.client.Minecraft; +import net.minecraft.network.chat.Component; public class ModMenuIntegration implements ModMenuApi { @Override @@ -19,10 +19,10 @@ public class ModMenuIntegration implements ModMenuApi { return parent -> { ConfigBuilder builder = ConfigBuilder.create() .setParentScreen(parent) - .setTitle(Text.translatable("title.spmega.config")); + .setTitle(Component.translatable("title.spmega.config")); ConfigEntryBuilder entryBuilder = builder.entryBuilder(); - ConfigCategory general = builder.getOrCreateCategory(Text.translatable("category.spmega.general")); + ConfigCategory general = builder.getOrCreateCategory(Component.translatable("category.spmega.general")); ModConfig current = SPMega.getConfig(); if (current == null) { @@ -39,27 +39,27 @@ public class ModMenuIntegration implements ModMenuApi { final int[] telemetryIntervalSeconds = {current.telemetryIntervalSeconds()}; final boolean[] telemetryCollectSystemInfo = {current.telemetryCollectSystemInfo()}; - general.addEntry(entryBuilder.startStrField(Text.translatable("option.spmega.api_domain"), current.apiDomain()) + general.addEntry(entryBuilder.startStrField(Component.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()) + general.addEntry(entryBuilder.startStrField(Component.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()) + general.addEntry(entryBuilder.startBooleanToggle(Component.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 и ваш бэкенд")) + general.addEntry(entryBuilder.startBooleanToggle(Component.literal("Авторизоваться").withStyle(ChatFormatting.RED), isLoggedIn) + .setTooltip(Component.literal("Запустить процесс авторизации через Mojang и ваш бэкенд")) .setSaveConsumer(newValue -> { if (newValue) { - BackendAuthenticator.authenticateAsync(MinecraftClient.getInstance()) + BackendAuthenticator.authenticateAsync(Minecraft.getInstance()) .thenAccept(authenticated -> { if (authenticated && SPMega.getConfig() != null) { apiToken[0] = SPMega.getConfig().apiToken(); @@ -71,46 +71,46 @@ public class ModMenuIntegration implements ModMenuApi { }) .build()); - general.addEntry(entryBuilder.startBooleanToggle(Text.translatable("option.spmega.sign_quick_pay"), current.signQuickPayEnabled()) + general.addEntry(entryBuilder.startBooleanToggle(Component.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"), + Component.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())) + .setEnumNameProvider(position -> Component.translatable("option.spmega.gps_position." + position.name().toLowerCase())) .setSaveConsumer(newValue -> gpsPosition[0] = newValue) .build()); general.addEntry(entryBuilder.startEnumSelector( - Text.translatable("option.spmega.notification_position"), + Component.translatable("option.spmega.notification_position"), GpsHudPosition.class, current.notificationPosition() ) .setDefaultValue(ModConfig.DEFAULT_NOTIFICATION_POSITION) - .setEnumNameProvider(position -> Text.translatable( + .setEnumNameProvider(position -> Component.translatable( "option.spmega.notification_position." + position.name().toLowerCase())) .setSaveConsumer(newValue -> notificationPosition[0] = newValue) .build()); // Telemetry settings - ConfigCategory telemetry = builder.getOrCreateCategory(Text.translatable("category.spmega.telemetry")); + ConfigCategory telemetry = builder.getOrCreateCategory(Component.translatable("category.spmega.telemetry")); - telemetry.addEntry(entryBuilder.startIntField(Text.translatable("option.spmega.telemetry_interval"), current.telemetryIntervalSeconds()) + telemetry.addEntry(entryBuilder.startIntField(Component.translatable("option.spmega.telemetry_interval"), current.telemetryIntervalSeconds()) .setDefaultValue(ModConfig.DEFAULT_TELEMETRY_INTERVAL_SECONDS) .setMin(10) .setMax(600) - .setTooltip(Text.translatable("tooltip.spmega.telemetry_interval")) + .setTooltip(Component.translatable("tooltip.spmega.telemetry_interval")) .setSaveConsumer(newValue -> telemetryIntervalSeconds[0] = newValue) .build()); - telemetry.addEntry(entryBuilder.startBooleanToggle(Text.translatable("option.spmega.telemetry_system_info"), current.telemetryCollectSystemInfo()) + telemetry.addEntry(entryBuilder.startBooleanToggle(Component.translatable("option.spmega.telemetry_system_info"), current.telemetryCollectSystemInfo()) .setDefaultValue(ModConfig.DEFAULT_TELEMETRY_COLLECT_SYSTEM_INFO) - .setTooltip(Text.translatable("tooltip.spmega.telemetry_system_info")) + .setTooltip(Component.translatable("tooltip.spmega.telemetry_system_info")) .setSaveConsumer(newValue -> telemetryCollectSystemInfo[0] = newValue) .build()); diff --git a/src/client/java/git/yawaflua/tech/spmega/client/SPMegaClient.java b/src/client/java/git/yawaflua/tech/spmega/client/SPMegaClient.java index 30128b6..b4a2bef 100644 --- a/src/client/java/git/yawaflua/tech/spmega/client/SPMegaClient.java +++ b/src/client/java/git/yawaflua/tech/spmega/client/SPMegaClient.java @@ -1,5 +1,6 @@ package git.yawaflua.tech.spmega.client; +import com.mojang.blaze3d.platform.InputConstants; import git.yawaflua.tech.spmega.ModConfig; import git.yawaflua.tech.spmega.SPMega; import git.yawaflua.tech.spmega.client.qr.QRCodeScanner; @@ -11,20 +12,24 @@ 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; -import net.fabricmc.fabric.api.client.keybinding.v1.KeyBindingHelper; +import net.fabricmc.fabric.api.client.keymapping.v1.KeyMappingHelper; import net.fabricmc.fabric.api.client.networking.v1.ClientPlayConnectionEvents; -import net.fabricmc.fabric.api.client.rendering.v1.HudRenderCallback; +/*? if mc_26 {*/ + import net.fabricmc.fabric.api.client.rendering.v1.hud.HudElementRegistry; + import net.minecraft.resources.Identifier; +/*?} else {*/ +/*import net.fabricmc.fabric.api.client.rendering.v1.HudRenderCallback; +*//*?}*/ import net.fabricmc.fabric.api.client.screen.v1.ScreenEvents; import net.fabricmc.fabric.api.event.player.UseBlockCallback; import net.fabricmc.fabric.api.event.player.UseEntityCallback; -import net.minecraft.block.entity.SignBlockEntity; -import net.minecraft.client.MinecraftClient; -import net.minecraft.client.option.KeyBinding; -import net.minecraft.client.util.InputUtil; -import net.minecraft.entity.decoration.ItemFrameEntity; -import net.minecraft.entity.player.PlayerEntity; -import net.minecraft.text.Text; -import net.minecraft.util.ActionResult; +import net.minecraft.client.KeyMapping; +import net.minecraft.client.Minecraft; +import net.minecraft.network.chat.Component; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.entity.decoration.ItemFrame; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.level.block.entity.SignBlockEntity; import org.lwjgl.glfw.GLFW; import java.util.regex.Matcher; @@ -32,9 +37,9 @@ import java.util.regex.Pattern; public class SPMegaClient implements ClientModInitializer { private static final Pattern ISOLATED_FIVE_DIGITS = Pattern.compile("(? { PerformanceSampler.instance().tick(); UiNotifications.instance().tick(); - while (openBankMenuKeyBinding.wasPressed()) { + BackendAuthenticator.tickTokenRefresh(client); + while (openBankMenuKeyBinding.consumeClick()) { UiOpeners.openMainMenu(client); } - while (scanQrKeyBinding.wasPressed()) { + while (scanQrKeyBinding.consumeClick()) { QRCodeScanner.scanQrCode(client); } - while (toggleGpsKeyBinding.wasPressed()) { + while (toggleGpsKeyBinding.consumeClick()) { GpsHudRenderer.instance().toggle(); if (SPMega.getConfig() != null) { ModConfig current = SPMega.getConfig(); @@ -126,15 +148,15 @@ public class SPMegaClient implements ClientModInitializer { SPMega.setConfig(updated); } String status = GpsHudRenderer.instance().isEnabled() ? "включен" : "выключен"; - UiNotifications.instance().show(Text.literal("GPS Ада " + status)); + UiNotifications.instance().show(Component.literal("GPS Ада " + status)); } if (client.player != null && client.options != null) { - boolean isCombinationPressed = client.options.sprintKey.isPressed() && client.options.sneakKey.isPressed() && client.options.leftKey.isPressed(); + boolean isCombinationPressed = client.options.keySprint.isDown() && client.options.keyShift.isDown() && client.options.keyLeft.isDown(); if (isCombinationPressed) { - if (!wasPaymentShortcutPressed && client.currentScreen == null) { - if (client.targetedEntity instanceof PlayerEntity targetedPlayer && targetedPlayer != client.player) { - String recipient = targetedPlayer.getStringifiedName(); + if (!wasPaymentShortcutPressed && client.gui.screen() == null) { + if (client.crosshairPickEntity instanceof Player targetedPlayer && targetedPlayer != client.player) { + String recipient = targetedPlayer.getScoreboardName(); UiOpeners.openPaymentMenu(client, recipient); } } @@ -145,31 +167,35 @@ public class SPMegaClient implements ClientModInitializer { } }); - HudRenderCallback.EVENT.register((drawContext, tickDeltaManager) -> { - MinecraftClient client = MinecraftClient.getInstance(); - if (client.currentScreen != null || client.textRenderer == null) { + /*? if mc_26 {*/ + HudElementRegistry.addLast(Identifier.fromNamespaceAndPath("spmega", "main_hud"), (drawContext, tickDeltaManager) -> { + /*?} else {*/ + /*HudRenderCallback.EVENT.register((drawContext, tickDeltaManager) -> { + *//*?}*/ + Minecraft client = Minecraft.getInstance(); + if (client.gui.screen() != null || client.font == null) { return; } UiNotifications.instance().render( drawContext, - client.textRenderer, - client.getWindow().getScaledWidth(), - client.getWindow().getScaledHeight() + client.font, + client.getWindow().getGuiScaledWidth(), + client.getWindow().getGuiScaledHeight() ); GpsHudRenderer.instance().render( drawContext, - client.textRenderer, - client.getWindow().getScaledWidth(), - client.getWindow().getScaledHeight() + client.font, + client.getWindow().getGuiScaledWidth(), + client.getWindow().getGuiScaledHeight() ); }); ScreenEvents.AFTER_INIT.register((client, screen, scaledWidth, scaledHeight) -> { - ScreenEvents.afterRender(screen).register((screenInstance, drawContext, mouseX, mouseY, tickDelta) -> { - if (client.textRenderer == null) { + ScreenEvents.afterExtract(screen).register((screenInstance, drawContext, mouseX, mouseY, tickDelta) -> { + if (client.font == null) { return; } - UiNotifications.instance().render(drawContext, client.textRenderer, scaledWidth, scaledHeight); + UiNotifications.instance().render(drawContext, client.font, scaledWidth, scaledHeight); }); } @@ -177,66 +203,61 @@ public class SPMegaClient implements ClientModInitializer { ClientPlayConnectionEvents.JOIN.register((handler, sender, client) -> { if (client.player != null) { - BankUiService.instance().refreshOnServerJoinAsync(client.player.getUuidAsString()); + BankUiService.instance().refreshOnServerJoinAsync(client.player.getStringUUID()); } }); UseBlockCallback.EVENT.register((player, world, hand, hitResult) -> { - if (!world.isClient()) { - return ActionResult.PASS; + if (!world.isClientSide()) { + return InteractionResult.PASS; } - if (!player.isSneaking()) { - return ActionResult.PASS; + if (!player.isShiftKeyDown()) { + return InteractionResult.PASS; } if (SPMega.getConfig() == null || !SPMega.getConfig().signQuickPayEnabled()) { - return ActionResult.PASS; + return InteractionResult.PASS; } if (world.getBlockEntity(hitResult.getBlockPos()) instanceof SignBlockEntity signBlockEntity) { String cardNumber = extractCardNumber(signBlockEntity); if (cardNumber == null) { - return ActionResult.PASS; + return InteractionResult.PASS; } - UiOpeners.openPaymentMenu(MinecraftClient.getInstance(), cardNumber); - return ActionResult.SUCCESS; + UiOpeners.openPaymentMenu(Minecraft.getInstance(), cardNumber); + return InteractionResult.SUCCESS; } else { - return ActionResult.PASS; + return InteractionResult.PASS; } }); UseEntityCallback.EVENT.register((player, world, hand, entity, hitResult) -> { - if (!world.isClient()) { - return ActionResult.PASS; + if (!world.isClientSide()) { + return InteractionResult.PASS; } - if (!player.isSneaking()) { - return ActionResult.PASS; + if (!player.isShiftKeyDown()) { + return InteractionResult.PASS; } if (SPMega.getConfig() == null || !SPMega.getConfig().signQuickPayEnabled()) { - return ActionResult.PASS; + return InteractionResult.PASS; } - if (entity instanceof ItemFrameEntity itemFrameEntity) { + if (entity instanceof ItemFrame itemFrameEntity) { String cardNumber = extractCardNumber(itemFrameEntity); if (cardNumber == null) { - return ActionResult.PASS; + return InteractionResult.PASS; } - UiOpeners.openPaymentMenu(MinecraftClient.getInstance(), cardNumber); - return ActionResult.SUCCESS; + UiOpeners.openPaymentMenu(Minecraft.getInstance(), cardNumber); + return InteractionResult.SUCCESS; } else { - return ActionResult.PASS; + return InteractionResult.PASS; } }); new ChatListener().register(); WebhookNotificationPoller.instance().start(); - BackendAuthenticator.authenticateAsync(MinecraftClient.getInstance()) - .exceptionally(throwable -> { - System.err.println("[SPMEGA] Error during async authenticate: " + throwable.getMessage()); - return null; - }); // Telemetry init long clientInitStart = System.nanoTime(); diff --git a/src/client/java/git/yawaflua/tech/spmega/client/WebhookNotificationPoller.java b/src/client/java/git/yawaflua/tech/spmega/client/WebhookNotificationPoller.java index 732122d..fa87a6c 100644 --- a/src/client/java/git/yawaflua/tech/spmega/client/WebhookNotificationPoller.java +++ b/src/client/java/git/yawaflua/tech/spmega/client/WebhookNotificationPoller.java @@ -3,13 +3,12 @@ package git.yawaflua.tech.spmega.client; import git.yawaflua.tech.spmega.client.ui.UiNotifications; import git.yawaflua.tech.spmega.client.ui.service.BackendAuthenticator; import git.yawaflua.tech.spmega.client.ui.service.BankUiService; -import net.minecraft.client.MinecraftClient; -import net.minecraft.text.Text; - import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; +import net.minecraft.client.Minecraft; +import net.minecraft.network.chat.Component; public final class WebhookNotificationPoller { private static final WebhookNotificationPoller INSTANCE = new WebhookNotificationPoller(); @@ -61,12 +60,12 @@ public final class WebhookNotificationPoller { System.err.println("[SPMEGA] Failed to poll webhook notifications: " + exception.getMessage()); return; } - MinecraftClient client = MinecraftClient.getInstance(); + Minecraft client = Minecraft.getInstance(); if (client == null || notifications.isEmpty()) { return; } client.execute(() -> notifications.forEach(notification -> - UiNotifications.instance().showQueued(Text.literal(format(notification))))); + UiNotifications.instance().showQueued(Component.literal(format(notification))))); }); } } diff --git a/src/client/java/git/yawaflua/tech/spmega/client/qr/QRCodeScanner.java b/src/client/java/git/yawaflua/tech/spmega/client/qr/QRCodeScanner.java index 73145f2..a8feef1 100644 --- a/src/client/java/git/yawaflua/tech/spmega/client/qr/QRCodeScanner.java +++ b/src/client/java/git/yawaflua/tech/spmega/client/qr/QRCodeScanner.java @@ -8,16 +8,16 @@ import git.yawaflua.tech.spmega.client.telemetry.TelemetryCollector; import git.yawaflua.tech.spmega.client.telemetry.TelemetryEvent; import git.yawaflua.tech.spmega.client.ui.QRcodeAcceptScreen; import git.yawaflua.tech.spmega.client.ui.UiNotifications; -import net.minecraft.client.MinecraftClient; -import net.minecraft.client.util.ScreenshotRecorder; -import net.minecraft.text.Text; - import java.util.Collections; import java.util.EnumMap; import java.util.Map; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; +import com.mojang.blaze3d.platform.NativeImage; +import net.minecraft.client.Minecraft; +import net.minecraft.client.Screenshot; +import net.minecraft.network.chat.Component; public final class QRCodeScanner { private static final ExecutorService DECODER = Executors.newSingleThreadExecutor(runnable -> { @@ -29,64 +29,85 @@ public final class QRCodeScanner { private QRCodeScanner() { } - public static void scanQrCode(MinecraftClient client) { - if (client == null || client.player == null || client.getFramebuffer() == null) { + public static void scanQrCode(Minecraft client) { + if (client == null || client.player == null || client.gameRenderer.mainRenderTarget() == null) { return; } UiNotifications notifications = UiNotifications.instance(); - int framebufferWidth = client.getWindow().getFramebufferWidth(); - int framebufferHeight = client.getWindow().getFramebufferHeight(); + int framebufferWidth = client.getWindow().getWidth(); + int framebufferHeight = client.getWindow().getHeight(); if (framebufferWidth <= 0 || framebufferHeight <= 0) { - client.player.sendMessage(Text.translatable("message.spmega.qr.capture_failed"), false); + /*? if mc_26 {*/ + client.player.sendSystemMessage(Component.translatable("message.spmega.qr.capture_failed")); + /*?} else {*/ + /*client.player.displayClientMessage(Component.translatable("message.spmega.qr.capture_failed"), false); + *//*?}*/ return; } long startNs = System.nanoTime(); try { - ScreenshotRecorder.takeScreenshot(client.getFramebuffer(), nativeImage -> { - try { - if (nativeImage == null || nativeImage.getWidth() <= 0 || nativeImage.getHeight() <= 0) { - client.execute(() -> { - if (client.player != null) { - notifications.show(Text.translatable("message.spmega.qr.capture_failed")); - } - }); - recordQrEvent(false, false, null, "capture_failed", startNs); - return; - } - - int width = nativeImage.getWidth(); - int height = nativeImage.getHeight(); - int[] pixels = nativeImage.copyPixelsArgb(); - CompletableFuture.supplyAsync(() -> decodeQrCode(width, height, pixels), DECODER) - .whenComplete((result, throwable) -> { - client.execute(() -> showResult(client, notifications, result, throwable)); - recordQrEvent( - throwable == null && result != null, - System.nanoTime() - startNs > 50_000_000L, - result, - throwable == null ? null : throwable.getClass().getSimpleName() + ": " + throwable.getMessage(), - startNs - ); - }); - } catch (Exception exception) { - client.execute(() -> notifications.show(Text.translatable("message.spmega.qr.error"))); - recordQrEvent(false, false, null, - exception.getClass().getSimpleName() + ": " + exception.getMessage(), startNs); - } finally { - if (nativeImage != null) { - nativeImage.close(); - } - } + /*? if mc_1_21_11 {*/ + Screenshot.takeScreenshot(client.gameRenderer.mainRenderTarget(), nativeImage -> { + processCapturedImage(client, notifications, nativeImage, startNs); }); + /*?} else {*/ + // processCapturedImage(client, notifications, Screenshot.takeScreenshot(client.gameRenderer.mainRenderTarget()), startNs); + /*?}*/ } catch (Exception ex) { - notifications.show(Text.translatable("message.spmega.qr.error")); + notifications.show(Component.translatable("message.spmega.qr.error")); recordQrEvent(false, false, null, ex.getClass().getSimpleName() + ": " + ex.getMessage(), startNs); } } + private static void processCapturedImage( + Minecraft client, + UiNotifications notifications, + NativeImage nativeImage, + long startNs + ) { + try { + if (nativeImage == null || nativeImage.getWidth() <= 0 || nativeImage.getHeight() <= 0) { + client.execute(() -> { + if (client.player != null) { + notifications.show(Component.translatable("message.spmega.qr.capture_failed")); + } + }); + recordQrEvent(false, false, null, "capture_failed", startNs); + return; + } + + int width = nativeImage.getWidth(); + int height = nativeImage.getHeight(); + /*? if mc_1_21_11 {*/ + int[] pixels = nativeImage.getPixels(); + /*?} else {*/ + // int[] pixels = nativeImage.getPixelsRGBA(); + /*?}*/ + CompletableFuture.supplyAsync(() -> decodeQrCode(width, height, pixels), DECODER) + .whenComplete((result, throwable) -> { + client.execute(() -> showResult(client, notifications, result, throwable)); + recordQrEvent( + throwable == null && result != null, + System.nanoTime() - startNs > 50_000_000L, + result, + throwable == null ? null : throwable.getClass().getSimpleName() + ": " + throwable.getMessage(), + startNs + ); + }); + } catch (Exception exception) { + client.execute(() -> notifications.show(Component.translatable("message.spmega.qr.error"))); + recordQrEvent(false, false, null, + exception.getClass().getSimpleName() + ": " + exception.getMessage(), startNs); + } finally { + if (nativeImage != null) { + nativeImage.close(); + } + } + } + private static void showResult( - MinecraftClient client, + Minecraft client, UiNotifications notifications, String result, Throwable throwable @@ -95,17 +116,21 @@ public final class QRCodeScanner { return; } if (throwable != null) { - notifications.show(Text.translatable("message.spmega.qr.error")); + notifications.show(Component.translatable("message.spmega.qr.error")); return; } if (result == null) { - notifications.show(Text.translatable("message.spmega.qr.not_found")); + notifications.show(Component.translatable("message.spmega.qr.not_found")); return; } - Text clickableLink = Text.literal(result).styled(style -> style.withInsertion(result)); - client.player.sendMessage(Text.translatable("message.spmega.qr.found_link", clickableLink), false); - client.setScreen(new QRcodeAcceptScreen(result, client.currentScreen)); + Component clickableLink = Component.literal(result).withStyle(style -> style.withInsertion(result)); + /*? if mc_26 {*/ + client.player.sendSystemMessage(Component.translatable("message.spmega.qr.found_link", clickableLink)); + /*?} else {*/ + /*client.player.displayClientMessage(Component.translatable("message.spmega.qr.found_link", clickableLink), false); + *//*?}*/ + client.gui.setScreen(new QRcodeAcceptScreen(result, client.gui.screen())); } private static void recordQrEvent(boolean success, boolean didLag, String decodedUrl, String error, long startNs) { @@ -165,4 +190,3 @@ public final class QRCodeScanner { return null; } } - diff --git a/src/client/java/git/yawaflua/tech/spmega/client/telemetry/InstrumentedHttpClient.java b/src/client/java/git/yawaflua/tech/spmega/client/telemetry/InstrumentedHttpClient.java index acff2a4..f286257 100644 --- a/src/client/java/git/yawaflua/tech/spmega/client/telemetry/InstrumentedHttpClient.java +++ b/src/client/java/git/yawaflua/tech/spmega/client/telemetry/InstrumentedHttpClient.java @@ -1,6 +1,7 @@ package git.yawaflua.tech.spmega.client.telemetry; import com.google.gson.JsonObject; +import git.yawaflua.tech.spmega.client.ui.service.BackendAuthenticator; import java.net.URI; import java.net.http.HttpClient; @@ -53,6 +54,12 @@ public record InstrumentedHttpClient(HttpClient delegate) { } else { recordHttpEvent(uri, method, response.statusCode(), durationMs, true, null, fpsBefore, fpsAfter); + if (response.statusCode() == 401 + && response.headers().firstValue("X-SPMega-Reauthenticate") + .map("true"::equalsIgnoreCase) + .orElse(false)) { + BackendAuthenticator.onReauthenticationRequired(); + } } }); } diff --git a/src/client/java/git/yawaflua/tech/spmega/client/telemetry/PerformanceSampler.java b/src/client/java/git/yawaflua/tech/spmega/client/telemetry/PerformanceSampler.java index e2149a3..c1d5821 100644 --- a/src/client/java/git/yawaflua/tech/spmega/client/telemetry/PerformanceSampler.java +++ b/src/client/java/git/yawaflua/tech/spmega/client/telemetry/PerformanceSampler.java @@ -1,10 +1,9 @@ package git.yawaflua.tech.spmega.client.telemetry; import com.google.gson.JsonObject; -import net.minecraft.client.MinecraftClient; - import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; +import net.minecraft.client.Minecraft; public final class PerformanceSampler { private static final PerformanceSampler INSTANCE = new PerformanceSampler(); @@ -23,7 +22,7 @@ public final class PerformanceSampler { } public void tick() { - int fps = MinecraftClient.getInstance().getCurrentFps(); + int fps = Minecraft.getInstance().getFps(); fpsSum.addAndGet(fps); fpsCount.incrementAndGet(); fpsMin.updateAndGet(prev -> Math.min(prev, fps)); @@ -31,7 +30,7 @@ public final class PerformanceSampler { } public int currentFps() { - return MinecraftClient.getInstance().getCurrentFps(); + return Minecraft.getInstance().getFps(); } public void emitSnapshot() { diff --git a/src/client/java/git/yawaflua/tech/spmega/client/telemetry/SystemInfoCollector.java b/src/client/java/git/yawaflua/tech/spmega/client/telemetry/SystemInfoCollector.java index 58675ab..4d60a92 100644 --- a/src/client/java/git/yawaflua/tech/spmega/client/telemetry/SystemInfoCollector.java +++ b/src/client/java/git/yawaflua/tech/spmega/client/telemetry/SystemInfoCollector.java @@ -1,7 +1,6 @@ package git.yawaflua.tech.spmega.client.telemetry; import com.google.gson.JsonObject; -import net.minecraft.client.MinecraftClient; import org.lwjgl.opengl.GL11; import oshi.SystemInfo; import oshi.hardware.CentralProcessor; @@ -16,6 +15,7 @@ import java.nio.file.Path; import java.time.Duration; import java.util.Arrays; import java.util.concurrent.atomic.AtomicReference; +import net.minecraft.client.Minecraft; public final class SystemInfoCollector { private static final SystemInfoCollector INSTANCE = new SystemInfoCollector(); @@ -58,7 +58,7 @@ public final class SystemInfoCollector { } private void collectGpuOnRenderThread() { - MinecraftClient.getInstance().execute(() -> { + Minecraft.getInstance().execute(() -> { try { gpuRenderer.set(GL11.glGetString(GL11.GL_RENDERER)); gpuVendor.set(GL11.glGetString(GL11.GL_VENDOR)); diff --git a/src/client/java/git/yawaflua/tech/spmega/client/ui/AddCardScreen.java b/src/client/java/git/yawaflua/tech/spmega/client/ui/AddCardScreen.java index da8cca1..3c2e8d4 100644 --- a/src/client/java/git/yawaflua/tech/spmega/client/ui/AddCardScreen.java +++ b/src/client/java/git/yawaflua/tech/spmega/client/ui/AddCardScreen.java @@ -1,26 +1,25 @@ package git.yawaflua.tech.spmega.client.ui; import git.yawaflua.tech.spmega.client.ui.service.BankUiService; -import net.minecraft.client.gui.DrawContext; -import net.minecraft.client.gui.screen.Screen; -import net.minecraft.client.gui.widget.ButtonWidget; -import net.minecraft.client.gui.widget.TextFieldWidget; -import net.minecraft.text.Text; - import java.util.function.Consumer; +import net.minecraft.client.gui.GuiGraphicsExtractor; +import net.minecraft.client.gui.components.Button; +import net.minecraft.client.gui.components.EditBox; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.network.chat.Component; public class AddCardScreen extends Screen { private final Screen parent; private final Consumer onSubmitMessage; private final BankUiService bankUiService = BankUiService.instance(); private final UiNotifications notifications = UiNotifications.instance(); - private TextFieldWidget cardIdField; - private TextFieldWidget cardTokenField; - private ButtonWidget addButton; - private ButtonWidget cancelButton; + private EditBox cardIdField; + private EditBox cardTokenField; + private Button addButton; + private Button cancelButton; public AddCardScreen(Screen parent, Consumer onSubmitMessage) { - super(Text.literal("Добавление карты")); + super(Component.literal("Добавление карты")); this.parent = parent; this.onSubmitMessage = onSubmitMessage; } @@ -30,66 +29,74 @@ public class AddCardScreen extends Screen { int centerX = this.width / 2; int startY = this.height / 2 - 50; - cardIdField = new TextFieldWidget(this.textRenderer, centerX - 140, startY, 280, 20, Text.literal("Card ID")); + cardIdField = new EditBox(this.font, centerX - 140, startY, 280, 20, Component.literal("Card ID")); cardIdField.setMaxLength(64); - cardIdField.setPlaceholder(Text.literal("ID карты (UUID)")); - this.addDrawableChild(cardIdField); + cardIdField.setHint(Component.literal("ID карты (UUID)")); + this.addRenderableWidget(cardIdField); - cardTokenField = new TextFieldWidget(this.textRenderer, centerX - 140, startY + 28, 280, 20, Text.literal("Card Token")); + cardTokenField = new EditBox(this.font, centerX - 140, startY + 28, 280, 20, Component.literal("Card Token")); cardTokenField.setMaxLength(128); - cardTokenField.setPlaceholder(Text.literal("Токен карты")); - this.addDrawableChild(cardTokenField); + cardTokenField.setHint(Component.literal("Токен карты")); + this.addRenderableWidget(cardTokenField); - addButton = this.addDrawableChild(ButtonWidget.builder(Text.literal("Добавить"), button -> submit()) - .dimensions(centerX - 140, startY + 60, 136, 20) + addButton = this.addRenderableWidget(Button.builder(Component.literal("Добавить"), button -> submit()) + .bounds(centerX - 140, startY + 60, 136, 20) .build()); - cancelButton = this.addDrawableChild(ButtonWidget.builder(Text.literal("Отмена"), button -> this.close()) - .dimensions(centerX + 4, startY + 60, 136, 20) + cancelButton = this.addRenderableWidget(Button.builder(Component.literal("Отмена"), button -> this.onClose()) + .bounds(centerX + 4, startY + 60, 136, 20) .build()); } @Override - public void close() { - if (this.client != null) { - this.client.setScreen(parent); + public void onClose() { + if (this.minecraft != null) { + this.minecraft.gui.setScreen(parent); } } @Override - public void render(DrawContext context, int mouseX, int mouseY, float delta) { - super.render(context, mouseX, mouseY, delta); + /*? if mc_26 {*/ + public void extractRenderState(GuiGraphicsExtractor context, int mouseX, int mouseY, float delta) { + /*?} else {*/ + /*public void render(GuiGraphicsExtractor context, int mouseX, int mouseY, float delta) { + *//*?}*/ + /*? if mc_26 {*/ + super.extractRenderState(context, mouseX, mouseY, delta); + /*?} else {*/ + /*super.render(context, mouseX, mouseY, delta); + *//*?}*/ int centerX = this.width / 2; int startY = this.height / 2 - 50; - context.drawCenteredTextWithShadow(this.textRenderer, this.title, centerX, 24, 0xFFFFFF); - context.drawTextWithShadow(this.textRenderer, Text.literal("Card ID (UUID):"), centerX - 140, startY - 10, 0xCCCCCC); - context.drawTextWithShadow(this.textRenderer, Text.literal("Card Token:"), centerX - 140, startY + 18, 0xCCCCCC); + context.centeredText(this.font, this.title, centerX, 24, 0xFFFFFF); + context.text(this.font, Component.literal("Card ID (UUID):"), centerX - 140, startY - 10, 0xCCCCCC); + context.text(this.font, Component.literal("Card Token:"), centerX - 140, startY + 18, 0xCCCCCC); } private void submit() { - String cardId = cardIdField.getText().trim(); - String cardToken = cardTokenField.getText().trim(); + String cardId = cardIdField.getValue().trim(); + String cardToken = cardTokenField.getValue().trim(); if (cardId.isEmpty() || cardToken.isEmpty()) { - notifications.show(Text.literal("Укажи cardId и cardToken")); + notifications.show(Component.literal("Укажи cardId и cardToken")); return; } - String playerUuid = this.client != null && this.client.player != null - ? this.client.player.getUuidAsString() + String playerUuid = this.minecraft != null && this.minecraft.player != null + ? this.minecraft.player.getStringUUID() : ""; - notifications.show(Text.literal("Проверка карты...")); + notifications.show(Component.literal("Проверка карты...")); addButton.active = false; cancelButton.active = false; bankUiService.addCardAsync(cardId, cardToken, playerUuid) .thenAccept(message -> { - if (this.client == null) { + if (this.minecraft == null) { return; } - this.client.execute(() -> { + this.minecraft.execute(() -> { notifications.showMessage(message); addButton.active = true; cancelButton.active = true; @@ -99,14 +106,14 @@ public class AddCardScreen extends Screen { } if (message.startsWith("Карта добавлена") || message.startsWith("Вы не владелец карты")) { - this.close(); + this.onClose(); } }); }) .exceptionally(exception -> { - if (this.client != null) { - this.client.execute(() -> { - notifications.show(Text.literal("Ошибка добавления карты: " + exception.getMessage())); + if (this.minecraft != null) { + this.minecraft.execute(() -> { + notifications.show(Component.literal("Ошибка добавления карты: " + exception.getMessage())); addButton.active = true; cancelButton.active = true; }); @@ -115,4 +122,3 @@ public class AddCardScreen extends Screen { }); } } - diff --git a/src/client/java/git/yawaflua/tech/spmega/client/ui/CardScreen.java b/src/client/java/git/yawaflua/tech/spmega/client/ui/CardScreen.java index 2983570..2a0e393 100644 --- a/src/client/java/git/yawaflua/tech/spmega/client/ui/CardScreen.java +++ b/src/client/java/git/yawaflua/tech/spmega/client/ui/CardScreen.java @@ -2,25 +2,24 @@ package git.yawaflua.tech.spmega.client.ui; import git.yawaflua.tech.spmega.client.ui.service.BankUiService; import git.yawaflua.tech.spmega.client.ui.service.CardViewModel; -import net.minecraft.client.gui.DrawContext; -import net.minecraft.client.gui.screen.ConfirmScreen; -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 net.minecraft.client.gui.GuiGraphicsExtractor; +import net.minecraft.client.gui.components.Button; +import net.minecraft.client.gui.screens.ConfirmScreen; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.network.chat.Component; public class CardScreen extends Screen { private final Screen parent; private final BankUiService bankUiService = BankUiService.instance(); private final UiNotifications notifications = UiNotifications.instance(); - private final List cardButtons = new ArrayList<>(); - private ButtonWidget historyButton; - private ButtonWidget webhookButton; + private final List