Add initial implementation of SPMega mod with banking UI and API integration
Java CI / build (push) Successful in 5m38s
Java CI / build (push) Successful in 5m38s
Signed-off-by: Dmitrii <computer@yawaflua.tech> Took 2 hours 53 minutes
This commit is contained in:
@@ -0,0 +1,102 @@
|
||||
name: Release CI
|
||||
|
||||
on:
|
||||
release:
|
||||
types:
|
||||
- published
|
||||
push:
|
||||
tags:
|
||||
- v*
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
build-and-upload-mod:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Resolve tag
|
||||
id: tag
|
||||
shell: bash
|
||||
run: |
|
||||
if [ -n "${{ github.event.release.tag_name }}" ]; then
|
||||
echo "value=${{ github.event.release.tag_name }}" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "value=${GITHUB_REF_NAME}" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: Check release version (>= 1.21.11)
|
||||
id: version_check
|
||||
shell: bash
|
||||
run: |
|
||||
TAG="${{ steps.tag.outputs.value }}"
|
||||
VERSION="${TAG#v}"
|
||||
MIN_VERSION="1.21.11"
|
||||
|
||||
if [ "$(printf '%s\n%s\n' "$MIN_VERSION" "$VERSION" | sort -V | head -n1)" = "$MIN_VERSION" ]; then
|
||||
echo "should_build=true" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "should_build=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: Set up JDK 21
|
||||
if: steps.version_check.outputs.should_build == 'true'
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: "21"
|
||||
cache: gradle
|
||||
|
||||
- name: Grant execute permission for Gradle
|
||||
if: steps.version_check.outputs.should_build == 'true'
|
||||
run: chmod +x ./gradlew
|
||||
|
||||
- name: Build mod
|
||||
if: steps.version_check.outputs.should_build == 'true'
|
||||
run: ./gradlew clean build
|
||||
|
||||
- name: Upload jars to GitHub release assets
|
||||
if: steps.version_check.outputs.should_build == 'true' && github.server_url == 'https://github.com'
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
tag_name: ${{ steps.tag.outputs.value }}
|
||||
files: |
|
||||
build/libs/*.jar
|
||||
|
||||
- name: Upload jars to Gitea release assets
|
||||
if: steps.version_check.outputs.should_build == 'true' && github.server_url != 'https://github.com'
|
||||
shell: bash
|
||||
env:
|
||||
API: ${{ github.api_url }}
|
||||
REPO: ${{ github.repository }}
|
||||
TAG: ${{ steps.tag.outputs.value }}
|
||||
TOKEN: ${{ secrets.GITEA_TOKEN }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
release_json="$(curl -fsS -H "Authorization: token $TOKEN" "$API/repos/$REPO/releases/tags/$TAG" || true)"
|
||||
|
||||
if [ -z "$release_json" ]; then
|
||||
release_json="$(curl -fsS -X POST \
|
||||
-H "Authorization: token $TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"tag_name\":\"$TAG\",\"name\":\"$TAG\",\"draft\":false,\"prerelease\":false}" \
|
||||
"$API/repos/$REPO/releases")"
|
||||
fi
|
||||
|
||||
release_id="$(python -c 'import sys,json; print(json.load(sys.stdin)["id"])' <<< "$release_json")"
|
||||
|
||||
for file in build/libs/*.jar; do
|
||||
[ -f "$file" ] || continue
|
||||
name="$(basename "$file")"
|
||||
curl -fsS -X POST \
|
||||
-H "Authorization: token $TOKEN" \
|
||||
-H "Content-Type: application/octet-stream" \
|
||||
--data-binary @"$file" \
|
||||
"$API/repos/$REPO/releases/$release_id/assets?name=$name"
|
||||
done
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
name: Java CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- "**"
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up JDK 21
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: "21"
|
||||
cache: gradle
|
||||
|
||||
- name: Grant execute permission for Gradle wrapper
|
||||
run: chmod +x ./gradlew
|
||||
|
||||
- name: Build with Gradle
|
||||
run: ./gradlew clean build --no-daemon
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
# MacOS DS_Store files
|
||||
.DS_Store
|
||||
|
||||
# Gradle cache folder
|
||||
.gradle
|
||||
|
||||
# Gradle build folder
|
||||
build
|
||||
|
||||
# IntelliJ
|
||||
out/
|
||||
.idea
|
||||
*.iml
|
||||
# mpeltonen/sbt-idea plugin
|
||||
.idea_modules/
|
||||
|
||||
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
|
||||
hs_err_pid*
|
||||
|
||||
# Common working directory
|
||||
run
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
LICENSE Agreement
|
||||
|
||||
Copyright (c) 2024-2026 Dmitrii 'yawaflua' Shimanskii. All rights reserved.
|
||||
|
||||
This project is licensed under the Creative Commons Attribution 4.0 International License (CC BY 4.0)
|
||||
Subject to the additional restrictive conditions below:
|
||||
|
||||
1. PROHIBITION OF USE:
|
||||
Any use, reproduction, distribution, or modification of the materials in this repository
|
||||
without the express written permission of the author (Dmitrii 'yawaflua' Shimanskii) is PROHIBITED.
|
||||
|
||||
2. MANDATORY ATTRIBUTION:
|
||||
In all allowed cases of use, the authorship of Dmitrii 'yawaflua' Shimanskii must be
|
||||
clearly and explicitly credited.
|
||||
|
||||
3. SPECIAL PROVISIONS FOR EXTERNAL CODE:
|
||||
Parts of the code affiliated with or derived from the repository
|
||||
https://github.com/Zadudoder/SPmHelper/commit/8b8139efdef4d2466a65a6ae1218521564116db1
|
||||
are distributed under the terms of the MIT License (MIT) as applied to forks.
|
||||
The original MIT license conditions for those specific parts must be preserved.
|
||||
|
||||
For permissions and inquiries, contact the author.
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
# SPMega
|
||||
|
||||
Клиентский мод Fabric с банковым UI, интеграцией с API `spworlds.ru` и локальным SQLite-кэшем.
|
||||
|
||||
## Что реализовано
|
||||
|
||||
- Открытие меню по `P` и через кнопку `SPMega` в меню `Esc`
|
||||
- Экран `Карты`:
|
||||
- список карт из локальной БД
|
||||
- удаление карты
|
||||
- обновление данных карты через API
|
||||
- добавление карты из конфига (`token.cardId` + `token.cardToken`)
|
||||
- Экран `Оплата`:
|
||||
- перевод по номеру карты
|
||||
- при вводе ника: загрузка карт игрока из API и выбор карты получателя
|
||||
- выполнение транзакции через API
|
||||
- Локальная БД `config/spmega.db`:
|
||||
- `cards` (id, token, number, name, balance, owner_uuid)
|
||||
- `transfer_history` (локальная история переводов)
|
||||
- Автообновление балансов при входе на сервер
|
||||
|
||||
## Ключевые файлы
|
||||
|
||||
- `src/main/java/git/yawaflua/tech/spmega/api/SPWorldsApiClient.java`
|
||||
- `src/client/java/git/yawaflua/tech/spmega/client/ui/service/BankUiService.java`
|
||||
- `src/client/java/git/yawaflua/tech/spmega/client/ui/service/BankDatabase.java`
|
||||
- `src/client/java/git/yawaflua/tech/spmega/client/ui/PaymentScreen.java`
|
||||
- `src/client/java/git/yawaflua/tech/spmega/client/ui/CardScreen.java`
|
||||
|
||||
## Конфиг
|
||||
|
||||
Файл: `config/spmega.properties`
|
||||
|
||||
- `api.domain=https://spworlds.ru`
|
||||
- `token.cardId=<UUID карты>`
|
||||
- `token.cardToken=<токен карты>`
|
||||
|
||||
При добавлении новой карты через UI выполняется проверка владельца через `GET /api/public/accounts/me`.
|
||||
Если UUID не совпадает с UUID игрока, показывается сообщение:
|
||||
`Вы не владелец карты. Часть функций может быть ограничена.`
|
||||
|
||||
## Проверка сборки (PowerShell)
|
||||
|
||||
```powershell
|
||||
$javaHome = 'C:/Users/yawaflua/AppData/Roaming/PrismLauncher/java/java-runtime-delta/'
|
||||
$env:JAVA_HOME = $javaHome
|
||||
$env:Path = "$($env:JAVA_HOME)bin;$env:Path"
|
||||
Set-Location 'C:\Users\yawaflua\IdeaProjects\untitled'
|
||||
.\gradlew.bat classes
|
||||
```
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 1.6 KiB |
+115
@@ -0,0 +1,115 @@
|
||||
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 {
|
||||
// Add repositories to retrieve artifacts from in here.
|
||||
// You should only use this when depending on other mods because
|
||||
// Loom adds the essential maven repositories to download Minecraft and libraries from automatically.
|
||||
// See https://docs.gradle.org/current/userguide/declaring_repositories.html
|
||||
// for more information about repositories.
|
||||
|
||||
maven { url "https://maven.shedaniel.me/" }
|
||||
}
|
||||
|
||||
dependencies {
|
||||
// To change the versions see the gradle.properties file
|
||||
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")
|
||||
}
|
||||
|
||||
include(implementation("org.xerial:sqlite-jdbc:3.46.1.3"))
|
||||
}
|
||||
|
||||
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.
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
# 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
|
||||
# Mod Properties
|
||||
mod_version=0.1-pre-alpha
|
||||
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
|
||||
Vendored
BIN
Binary file not shown.
+7
@@ -0,0 +1,7 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-9.2.1-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
@@ -0,0 +1,248 @@
|
||||
#!/bin/sh
|
||||
|
||||
#
|
||||
# Copyright © 2015 the original authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
|
||||
##############################################################################
|
||||
#
|
||||
# Gradle start up script for POSIX generated by Gradle.
|
||||
#
|
||||
# Important for running:
|
||||
#
|
||||
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
|
||||
# noncompliant, but you have some other compliant shell such as ksh or
|
||||
# bash, then to run this script, type that shell name before the whole
|
||||
# command line, like:
|
||||
#
|
||||
# ksh Gradle
|
||||
#
|
||||
# Busybox and similar reduced shells will NOT work, because this script
|
||||
# requires all of these POSIX shell features:
|
||||
# * functions;
|
||||
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
|
||||
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
|
||||
# * compound commands having a testable exit status, especially «case»;
|
||||
# * various built-in commands including «command», «set», and «ulimit».
|
||||
#
|
||||
# Important for patching:
|
||||
#
|
||||
# (2) This script targets any POSIX shell, so it avoids extensions provided
|
||||
# by Bash, Ksh, etc; in particular arrays are avoided.
|
||||
#
|
||||
# The "traditional" practice of packing multiple parameters into a
|
||||
# space-separated string is a well documented source of bugs and security
|
||||
# problems, so this is (mostly) avoided, by progressively accumulating
|
||||
# options in "$@", and eventually passing that to Java.
|
||||
#
|
||||
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
|
||||
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
|
||||
# see the in-line comments for details.
|
||||
#
|
||||
# There are tweaks for specific operating systems such as AIX, CygWin,
|
||||
# Darwin, MinGW, and NonStop.
|
||||
#
|
||||
# (3) This script is generated from the Groovy template
|
||||
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||
# within the Gradle project.
|
||||
#
|
||||
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
# Attempt to set APP_HOME
|
||||
|
||||
# Resolve links: $0 may be a link
|
||||
app_path=$0
|
||||
|
||||
# Need this for daisy-chained symlinks.
|
||||
while
|
||||
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
|
||||
[ -h "$app_path" ]
|
||||
do
|
||||
ls=$( ls -ld "$app_path" )
|
||||
link=${ls#*' -> '}
|
||||
case $link in #(
|
||||
/*) app_path=$link ;; #(
|
||||
*) app_path=$APP_HOME$link ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# This is normally unused
|
||||
# shellcheck disable=SC2034
|
||||
APP_BASE_NAME=${0##*/}
|
||||
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
|
||||
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD=maximum
|
||||
|
||||
warn () {
|
||||
echo "$*"
|
||||
} >&2
|
||||
|
||||
die () {
|
||||
echo
|
||||
echo "$*"
|
||||
echo
|
||||
exit 1
|
||||
} >&2
|
||||
|
||||
# OS specific support (must be 'true' or 'false').
|
||||
cygwin=false
|
||||
msys=false
|
||||
darwin=false
|
||||
nonstop=false
|
||||
case "$( uname )" in #(
|
||||
CYGWIN* ) cygwin=true ;; #(
|
||||
Darwin* ) darwin=true ;; #(
|
||||
MSYS* | MINGW* ) msys=true ;; #(
|
||||
NONSTOP* ) nonstop=true ;;
|
||||
esac
|
||||
|
||||
|
||||
|
||||
# Determine the Java command to use to start the JVM.
|
||||
if [ -n "$JAVA_HOME" ] ; then
|
||||
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||
# IBM's JDK on AIX uses strange locations for the executables
|
||||
JAVACMD=$JAVA_HOME/jre/sh/java
|
||||
else
|
||||
JAVACMD=$JAVA_HOME/bin/java
|
||||
fi
|
||||
if [ ! -x "$JAVACMD" ] ; then
|
||||
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
else
|
||||
JAVACMD=java
|
||||
if ! command -v java >/dev/null 2>&1
|
||||
then
|
||||
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
fi
|
||||
|
||||
# Increase the maximum file descriptors if we can.
|
||||
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||
case $MAX_FD in #(
|
||||
max*)
|
||||
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC2039,SC3045
|
||||
MAX_FD=$( ulimit -H -n ) ||
|
||||
warn "Could not query maximum file descriptor limit"
|
||||
esac
|
||||
case $MAX_FD in #(
|
||||
'' | soft) :;; #(
|
||||
*)
|
||||
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC2039,SC3045
|
||||
ulimit -n "$MAX_FD" ||
|
||||
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
||||
esac
|
||||
fi
|
||||
|
||||
# Collect all arguments for the java command, stacking in reverse order:
|
||||
# * args from the command line
|
||||
# * the main class name
|
||||
# * -classpath
|
||||
# * -D...appname settings
|
||||
# * --module-path (only if needed)
|
||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
|
||||
|
||||
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||
if "$cygwin" || "$msys" ; then
|
||||
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
|
||||
|
||||
JAVACMD=$( cygpath --unix "$JAVACMD" )
|
||||
|
||||
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||
for arg do
|
||||
if
|
||||
case $arg in #(
|
||||
-*) false ;; # don't mess with options #(
|
||||
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
|
||||
[ -e "$t" ] ;; #(
|
||||
*) false ;;
|
||||
esac
|
||||
then
|
||||
arg=$( cygpath --path --ignore --mixed "$arg" )
|
||||
fi
|
||||
# Roll the args list around exactly as many times as the number of
|
||||
# args, so each arg winds up back in the position where it started, but
|
||||
# possibly modified.
|
||||
#
|
||||
# NB: a `for` loop captures its iteration list before it begins, so
|
||||
# changing the positional parameters here affects neither the number of
|
||||
# iterations, nor the values presented in `arg`.
|
||||
shift # remove old arg
|
||||
set -- "$@" "$arg" # push replacement arg
|
||||
done
|
||||
fi
|
||||
|
||||
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||
|
||||
# Collect all arguments for the java command:
|
||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
|
||||
# and any embedded shellness will be escaped.
|
||||
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
|
||||
# treated as '${Hostname}' itself on the command line.
|
||||
|
||||
set -- \
|
||||
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
|
||||
"$@"
|
||||
|
||||
# Stop when "xargs" is not available.
|
||||
if ! command -v xargs >/dev/null 2>&1
|
||||
then
|
||||
die "xargs is not available"
|
||||
fi
|
||||
|
||||
# Use "xargs" to parse quoted args.
|
||||
#
|
||||
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
|
||||
#
|
||||
# In Bash we could simply go:
|
||||
#
|
||||
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
|
||||
# set -- "${ARGS[@]}" "$@"
|
||||
#
|
||||
# but POSIX shell has neither arrays nor command substitution, so instead we
|
||||
# post-process each arg (as a line of input to sed) to backslash-escape any
|
||||
# character that might be a shell metacharacter, then use eval to reverse
|
||||
# that process (while maintaining the separation between arguments), and wrap
|
||||
# the whole thing up as a single "set" statement.
|
||||
#
|
||||
# This will of course break if any of these variables contains a newline or
|
||||
# an unmatched quote.
|
||||
#
|
||||
|
||||
eval "set -- $(
|
||||
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
|
||||
xargs -n1 |
|
||||
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
|
||||
tr '\n' ' '
|
||||
)" '"$@"'
|
||||
|
||||
exec "$JAVACMD" "$@"
|
||||
Vendored
+93
@@ -0,0 +1,93 @@
|
||||
@rem
|
||||
@rem Copyright 2015 the original author or authors.
|
||||
@rem
|
||||
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@rem you may not use this file except in compliance with the License.
|
||||
@rem You may obtain a copy of the License at
|
||||
@rem
|
||||
@rem https://www.apache.org/licenses/LICENSE-2.0
|
||||
@rem
|
||||
@rem Unless required by applicable law or agreed to in writing, software
|
||||
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
||||
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
@rem See the License for the specific language governing permissions and
|
||||
@rem limitations under the License.
|
||||
@rem
|
||||
@rem SPDX-License-Identifier: Apache-2.0
|
||||
@rem
|
||||
|
||||
@if "%DEBUG%"=="" @echo off
|
||||
@rem ##########################################################################
|
||||
@rem
|
||||
@rem Gradle startup script for Windows
|
||||
@rem
|
||||
@rem ##########################################################################
|
||||
|
||||
@rem Set local scope for the variables with windows NT shell
|
||||
if "%OS%"=="Windows_NT" setlocal
|
||||
|
||||
set DIRNAME=%~dp0
|
||||
if "%DIRNAME%"=="" set DIRNAME=.
|
||||
@rem This is normally unused
|
||||
set APP_BASE_NAME=%~n0
|
||||
set APP_HOME=%DIRNAME%
|
||||
|
||||
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
||||
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
||||
|
||||
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
||||
|
||||
@rem Find java.exe
|
||||
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||
|
||||
set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if %ERRORLEVEL% equ 0 goto execute
|
||||
|
||||
echo. 1>&2
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
|
||||
echo. 1>&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||
echo location of your Java installation. 1>&2
|
||||
|
||||
goto fail
|
||||
|
||||
:findJavaFromJavaHome
|
||||
set JAVA_HOME=%JAVA_HOME:"=%
|
||||
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||
|
||||
if exist "%JAVA_EXE%" goto execute
|
||||
|
||||
echo. 1>&2
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
|
||||
echo. 1>&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||
echo location of your Java installation. 1>&2
|
||||
|
||||
goto fail
|
||||
|
||||
:execute
|
||||
@rem Setup the command line
|
||||
|
||||
|
||||
|
||||
@rem Execute Gradle
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
|
||||
|
||||
:end
|
||||
@rem End local scope for the variables with windows NT shell
|
||||
if %ERRORLEVEL% equ 0 goto mainEnd
|
||||
|
||||
:fail
|
||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||
rem the _cmd.exe /c_ return code!
|
||||
set EXIT_CODE=%ERRORLEVEL%
|
||||
if %EXIT_CODE% equ 0 set EXIT_CODE=1
|
||||
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
|
||||
exit /b %EXIT_CODE%
|
||||
|
||||
:mainEnd
|
||||
if "%OS%"=="Windows_NT" endlocal
|
||||
|
||||
:omega
|
||||
@@ -0,0 +1,9 @@
|
||||
pluginManagement {
|
||||
repositories {
|
||||
maven {
|
||||
name = 'Fabric'
|
||||
url = 'https://maven.fabricmc.net/'
|
||||
}
|
||||
gradlePluginPortal()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
package git.yawaflua.tech.spmega.client;
|
||||
|
||||
import git.yawaflua.tech.spmega.SPMega;
|
||||
import git.yawaflua.tech.spmega.client.ui.UiOpeners;
|
||||
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.networking.v1.ClientPlayConnectionEvents;
|
||||
import net.fabricmc.fabric.api.event.player.UseBlockCallback;
|
||||
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.text.Text;
|
||||
import net.minecraft.util.ActionResult;
|
||||
import org.lwjgl.glfw.GLFW;
|
||||
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public class SPMegaClient implements ClientModInitializer {
|
||||
private static final Pattern ISOLATED_FIVE_DIGITS = Pattern.compile("(?<!\\d)(\\d{5})(?!\\d)");
|
||||
private static KeyBinding openBankMenuKeyBinding;
|
||||
|
||||
private static String extractCardNumber(SignBlockEntity signBlockEntity) {
|
||||
String candidate = findFiveDigits(signBlockEntity.getFrontText().getMessages(false));
|
||||
if (candidate != null) {
|
||||
return candidate;
|
||||
}
|
||||
return findFiveDigits(signBlockEntity.getBackText().getMessages(false));
|
||||
}
|
||||
|
||||
private static String findFiveDigits(Text[] lines) {
|
||||
for (Text line : lines) {
|
||||
Matcher matcher = ISOLATED_FIVE_DIGITS.matcher(line.getString());
|
||||
if (matcher.find()) {
|
||||
return matcher.group(1);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onInitializeClient() {
|
||||
openBankMenuKeyBinding = KeyBindingHelper.registerKeyBinding(new KeyBinding(
|
||||
"key.spmega.open_menu",
|
||||
InputUtil.Type.KEYSYM,
|
||||
GLFW.GLFW_KEY_P,
|
||||
KeyBinding.Category.GAMEPLAY
|
||||
));
|
||||
|
||||
ClientTickEvents.END_CLIENT_TICK.register(client -> {
|
||||
while (openBankMenuKeyBinding.wasPressed()) {
|
||||
UiOpeners.openMainMenu(client);
|
||||
}
|
||||
});
|
||||
|
||||
ClientPlayConnectionEvents.JOIN.register((handler, sender, client) -> {
|
||||
if (client.player != null) {
|
||||
BankUiService.instance().refreshOnServerJoin(client.player.getUuidAsString());
|
||||
}
|
||||
});
|
||||
|
||||
UseBlockCallback.EVENT.register((player, world, hand, hitResult) -> {
|
||||
if (!world.isClient()) {
|
||||
return ActionResult.PASS;
|
||||
}
|
||||
if (!player.isSneaking()) {
|
||||
return ActionResult.PASS;
|
||||
}
|
||||
if (SPMega.getConfig() == null || !SPMega.getConfig().signQuickPayEnabled()) {
|
||||
return ActionResult.PASS;
|
||||
}
|
||||
|
||||
if (!(world.getBlockEntity(hitResult.getBlockPos()) instanceof SignBlockEntity signBlockEntity)) {
|
||||
return ActionResult.PASS;
|
||||
}
|
||||
|
||||
String cardNumber = extractCardNumber(signBlockEntity);
|
||||
if (cardNumber == null) {
|
||||
return ActionResult.PASS;
|
||||
}
|
||||
|
||||
UiOpeners.openPaymentMenu(MinecraftClient.getInstance(), cardNumber);
|
||||
return ActionResult.SUCCESS;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package git.yawaflua.tech.spmega.client;
|
||||
|
||||
import net.fabricmc.fabric.api.datagen.v1.DataGeneratorEntrypoint;
|
||||
import net.fabricmc.fabric.api.datagen.v1.FabricDataGenerator;
|
||||
|
||||
public class SPMegaDataGenerator implements DataGeneratorEntrypoint {
|
||||
|
||||
@Override
|
||||
public void onInitializeDataGenerator(FabricDataGenerator fabricDataGenerator) {
|
||||
FabricDataGenerator.Pack pack = fabricDataGenerator.createPack();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
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.concurrent.CompletableFuture;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
public class AddCardScreen extends Screen {
|
||||
private final Screen parent;
|
||||
private final Consumer<String> 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;
|
||||
|
||||
public AddCardScreen(Screen parent, Consumer<String> onSubmitMessage) {
|
||||
super(Text.literal("Добавление карты"));
|
||||
this.parent = parent;
|
||||
this.onSubmitMessage = onSubmitMessage;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void init() {
|
||||
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.setMaxLength(64);
|
||||
cardIdField.setPlaceholder(Text.literal("ID карты (UUID)"));
|
||||
this.addDrawableChild(cardIdField);
|
||||
|
||||
cardTokenField = new TextFieldWidget(this.textRenderer, centerX - 140, startY + 28, 280, 20, Text.literal("Card Token"));
|
||||
cardTokenField.setMaxLength(128);
|
||||
cardTokenField.setPlaceholder(Text.literal("Токен карты"));
|
||||
this.addDrawableChild(cardTokenField);
|
||||
|
||||
addButton = this.addDrawableChild(ButtonWidget.builder(Text.literal("Добавить"), button -> submit())
|
||||
.dimensions(centerX - 140, startY + 60, 136, 20)
|
||||
.build());
|
||||
|
||||
cancelButton = this.addDrawableChild(ButtonWidget.builder(Text.literal("Отмена"), button -> this.close())
|
||||
.dimensions(centerX + 4, startY + 60, 136, 20)
|
||||
.build());
|
||||
}
|
||||
|
||||
@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 = 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);
|
||||
notifications.render(context, this.textRenderer, this.width, this.height);
|
||||
}
|
||||
|
||||
private void submit() {
|
||||
String cardId = cardIdField.getText().trim();
|
||||
String cardToken = cardTokenField.getText().trim();
|
||||
if (cardId.isEmpty() || cardToken.isEmpty()) {
|
||||
notifications.show(Text.literal("Укажи cardId и cardToken"));
|
||||
return;
|
||||
}
|
||||
|
||||
String playerUuid = this.client != null && this.client.player != null
|
||||
? this.client.player.getUuidAsString()
|
||||
: "";
|
||||
|
||||
notifications.show(Text.literal("Проверка карты..."));
|
||||
addButton.active = false;
|
||||
cancelButton.active = false;
|
||||
|
||||
CompletableFuture
|
||||
.supplyAsync(() -> bankUiService.addCard(cardId, cardToken, playerUuid))
|
||||
.thenAccept(message -> {
|
||||
if (this.client == null) {
|
||||
return;
|
||||
}
|
||||
this.client.execute(() -> {
|
||||
notifications.showMessage(message);
|
||||
addButton.active = true;
|
||||
cancelButton.active = true;
|
||||
|
||||
if (onSubmitMessage != null) {
|
||||
onSubmitMessage.accept(message);
|
||||
}
|
||||
|
||||
if (message.startsWith("Карта добавлена") || message.startsWith("Вы не владелец карты")) {
|
||||
this.close();
|
||||
}
|
||||
});
|
||||
})
|
||||
.exceptionally(exception -> {
|
||||
if (this.client != null) {
|
||||
this.client.execute(() -> {
|
||||
notifications.show(Text.literal("Ошибка добавления карты: " + exception.getMessage()));
|
||||
addButton.active = true;
|
||||
cancelButton.active = true;
|
||||
});
|
||||
}
|
||||
return null;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
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.Screen;
|
||||
import net.minecraft.client.gui.widget.ButtonWidget;
|
||||
import net.minecraft.text.Text;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
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<>();
|
||||
|
||||
public CardScreen(Screen parent) {
|
||||
super(Text.literal("Управление картами"));
|
||||
this.parent = parent;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void init() {
|
||||
cardButtons.clear();
|
||||
|
||||
int centerX = this.width / 2;
|
||||
int startY = this.height / 2 - 82;
|
||||
int leftX = centerX - 210;
|
||||
int rightX = centerX + 14;
|
||||
int columnWidth = 196;
|
||||
|
||||
List<CardViewModel> cards = bankUiService.getCards();
|
||||
for (int i = 0; i < cards.size(); i++) {
|
||||
final int index = i;
|
||||
CardViewModel card = cards.get(i);
|
||||
ButtonWidget cardButton = this.addDrawableChild(ButtonWidget.builder(Text.literal(card.title()), button -> {
|
||||
bankUiService.setSelectedCardIndex(index);
|
||||
notifications.show(Text.literal("Выбрана карта " + card.title()));
|
||||
updateCardButtonStates();
|
||||
}).dimensions(leftX, startY + i * 24, columnWidth, 20).build());
|
||||
cardButtons.add(cardButton);
|
||||
}
|
||||
|
||||
this.addDrawableChild(ButtonWidget.builder(Text.literal("Удалить"), button -> {
|
||||
bankUiService.removeSelectedCard();
|
||||
notifications.showMessage(bankUiService.getLastMessage());
|
||||
this.clearAndInit();
|
||||
}).dimensions(rightX, startY, columnWidth, 20).build());
|
||||
|
||||
this.addDrawableChild(ButtonWidget.builder(Text.literal("Изменить"), button -> {
|
||||
String playerUuid = this.client != null && this.client.player != null
|
||||
? this.client.player.getUuidAsString()
|
||||
: "";
|
||||
bankUiService.refreshSelectedCard(playerUuid);
|
||||
String message = bankUiService.getLastMessage().isBlank() ? "Карта обновлена" : bankUiService.getLastMessage();
|
||||
notifications.showMessage(message);
|
||||
this.clearAndInit();
|
||||
}).dimensions(rightX, startY + 24, columnWidth, 20).build());
|
||||
|
||||
this.addDrawableChild(ButtonWidget.builder(Text.literal("Добавить новую"), button -> {
|
||||
if (this.client == null) {
|
||||
return;
|
||||
}
|
||||
this.client.setScreen(new AddCardScreen(this, message -> {
|
||||
notifications.showMessage(message);
|
||||
this.clearAndInit();
|
||||
}));
|
||||
}).dimensions(rightX, startY + 48, columnWidth, 20).build());
|
||||
|
||||
this.addDrawableChild(ButtonWidget.builder(Text.literal("Назад"), button -> this.close())
|
||||
.dimensions(rightX, startY + 120, columnWidth, 20)
|
||||
.build());
|
||||
|
||||
if (cards.isEmpty()) {
|
||||
notifications.show(Text.literal("Нет карт. Добавь карту через кнопку 'Добавить новую'."));
|
||||
}
|
||||
|
||||
updateCardButtonStates();
|
||||
}
|
||||
|
||||
@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 = this.height / 2 - 82;
|
||||
int leftX = centerX - 210;
|
||||
int rightX = centerX + 14;
|
||||
|
||||
context.drawCenteredTextWithShadow(this.textRenderer, this.title, centerX, 24, 0xFFFFFF);
|
||||
context.drawTextWithShadow(this.textRenderer, Text.literal("Список карт"), leftX, startY - 18, 0xBFBFBF);
|
||||
context.drawTextWithShadow(this.textRenderer, Text.literal("Действия"), rightX, startY - 18, 0xBFBFBF);
|
||||
notifications.render(context, this.textRenderer, this.width, this.height);
|
||||
}
|
||||
|
||||
private void updateCardButtonStates() {
|
||||
int selectedIndex = bankUiService.getSelectedCardIndex();
|
||||
List<CardViewModel> cards = bankUiService.getCards();
|
||||
|
||||
for (int i = 0; i < cardButtons.size(); i++) {
|
||||
ButtonWidget button = cardButtons.get(i);
|
||||
if (i >= cards.size()) {
|
||||
button.visible = false;
|
||||
button.active = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
boolean selected = i == selectedIndex;
|
||||
String prefix = selected ? ">> " : "";
|
||||
String suffix = selected ? " <<" : "";
|
||||
button.setMessage(Text.literal(prefix + cards.get(i).title() + suffix));
|
||||
button.active = !selected;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package git.yawaflua.tech.spmega.client.ui;
|
||||
|
||||
import net.minecraft.client.MinecraftClient;
|
||||
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;
|
||||
|
||||
public class MainBankScreen extends Screen {
|
||||
private final Screen parent;
|
||||
|
||||
public MainBankScreen(Screen parent) {
|
||||
super(Text.literal("SPMega"));
|
||||
this.parent = parent;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void init() {
|
||||
int centerX = this.width / 2;
|
||||
int y = this.height / 2 + 4;
|
||||
int buttonWidth = 92;
|
||||
int gap = 8;
|
||||
int totalWidth = buttonWidth * 3 + gap * 2;
|
||||
int startX = centerX - totalWidth / 2;
|
||||
|
||||
this.addDrawableChild(ButtonWidget.builder(Text.literal("Карта"), button -> {
|
||||
this.client.setScreen(new CardScreen(this));
|
||||
}).dimensions(startX, y, buttonWidth, 20).build());
|
||||
|
||||
this.addDrawableChild(ButtonWidget.builder(Text.literal("Оплата"), button -> {
|
||||
this.client.setScreen(new PaymentScreen(this));
|
||||
}).dimensions(startX + buttonWidth + gap, y, buttonWidth, 20).build());
|
||||
|
||||
this.addDrawableChild(ButtonWidget.builder(Text.literal("Закрыть"), button -> this.close())
|
||||
.dimensions(startX + (buttonWidth + gap) * 2, y, buttonWidth, 20)
|
||||
.build());
|
||||
}
|
||||
|
||||
@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);
|
||||
|
||||
context.drawCenteredTextWithShadow(this.textRenderer, this.title, this.width / 2, 24, 0xFFFFFF);
|
||||
context.drawCenteredTextWithShadow(this.textRenderer, Text.literal(greetingLine()), this.width / 2, 48, 0xFFD27F);
|
||||
}
|
||||
|
||||
private String greetingLine() {
|
||||
MinecraftClient minecraftClient = MinecraftClient.getInstance();
|
||||
String username = minecraftClient.getSession().getUsername();
|
||||
return "Доброе " + getTimeOfDay() + ", " + username;
|
||||
}
|
||||
|
||||
private String getTimeOfDay() {
|
||||
MinecraftClient minecraftClient = MinecraftClient.getInstance();
|
||||
if (minecraftClient.world == null) {
|
||||
return "время суток";
|
||||
}
|
||||
|
||||
long dayTime = minecraftClient.world.getTimeOfDay() % 24000L;
|
||||
if (dayTime < 6000) {
|
||||
return "утро";
|
||||
}
|
||||
if (dayTime < 12000) {
|
||||
return "день";
|
||||
}
|
||||
if (dayTime < 18000) {
|
||||
return "вечер";
|
||||
}
|
||||
return "ночь";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,384 @@
|
||||
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 git.yawaflua.tech.spmega.client.ui.service.PaymentDraft;
|
||||
import net.minecraft.client.font.TextRenderer;
|
||||
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.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public class PaymentScreen extends Screen {
|
||||
private static final int MAX_RECIPIENT_OPTION_BUTTONS = 10;
|
||||
private static final long RECIPIENT_LOOKUP_DEBOUNCE_MS = 500L;
|
||||
private static final Pattern CARD_ID_PATTERN = Pattern.compile("\\d{5}");
|
||||
private final Screen parent;
|
||||
private final BankUiService bankUiService = BankUiService.instance();
|
||||
private final List<ButtonWidget> recipientCardOptionButtons = new ArrayList<>();
|
||||
private final List<String> recipientCardOptions = new ArrayList<>();
|
||||
private final String initialRecipient;
|
||||
private final UiNotifications notifications = UiNotifications.instance();
|
||||
private TextFieldWidget amountField;
|
||||
private TextFieldWidget recipientField;
|
||||
private TextFieldWidget commentField;
|
||||
private ButtonWidget senderLeftButton;
|
||||
private ButtonWidget senderCardLabelButton;
|
||||
private ButtonWidget senderRightButton;
|
||||
private ButtonWidget recipientCardDropdownButton;
|
||||
private boolean recipientDropdownExpanded;
|
||||
private String selectedRecipientCard = "";
|
||||
private String lastRecipientLookup = "";
|
||||
private String pendingRecipientLookup = "";
|
||||
private long pendingRecipientLookupAt;
|
||||
private Text senderCardText = Text.literal("Карта отправителя: не выбрана");
|
||||
private ButtonWidget transferButton;
|
||||
private ButtonWidget backButton;
|
||||
|
||||
public PaymentScreen(Screen parent) {
|
||||
this(parent, "");
|
||||
}
|
||||
|
||||
public PaymentScreen(Screen parent, String initialRecipient) {
|
||||
super(Text.literal("Оплата"));
|
||||
this.parent = parent;
|
||||
this.initialRecipient = initialRecipient == null ? "" : initialRecipient.trim();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void init() {
|
||||
int centerX = this.width / 2;
|
||||
int startY = this.height / 2 - 82;
|
||||
int leftX = centerX - 210;
|
||||
int rightX = centerX + 14;
|
||||
int leftWidth = 196;
|
||||
int rightWidth = 196;
|
||||
TextRenderer tr = this.textRenderer;
|
||||
|
||||
amountField = new TextFieldWidget(tr, leftX, startY, leftWidth, 20, Text.literal("Сумма"));
|
||||
amountField.setMaxLength(16);
|
||||
amountField.setPlaceholder(Text.literal("Сумма перевода"));
|
||||
this.addDrawableChild(amountField);
|
||||
|
||||
recipientField = new TextFieldWidget(tr, leftX, startY + 28, leftWidth, 20, Text.literal("Получатель"));
|
||||
recipientField.setMaxLength(32);
|
||||
recipientField.setPlaceholder(Text.literal("Ник или 5 цифр карты"));
|
||||
recipientField.setChangedListener(value -> {
|
||||
String input = value.trim();
|
||||
if (isNickname(input)) {
|
||||
scheduleRecipientLookup(input);
|
||||
} else {
|
||||
lastRecipientLookup = "";
|
||||
pendingRecipientLookup = "";
|
||||
recipientCardOptions.clear();
|
||||
selectedRecipientCard = "";
|
||||
}
|
||||
updateRecipientDropdownVisibility();
|
||||
updateSenderCardText();
|
||||
});
|
||||
this.addDrawableChild(recipientField);
|
||||
|
||||
commentField = new TextFieldWidget(tr, leftX, startY + 56, leftWidth, 20, Text.literal("Комментарий"));
|
||||
commentField.setMaxLength(64);
|
||||
commentField.setPlaceholder(Text.literal("Комментарий (необязательно)"));
|
||||
this.addDrawableChild(commentField);
|
||||
|
||||
recipientCardDropdownButton = this.addDrawableChild(ButtonWidget.builder(Text.literal("Карта игрока: -"), button -> {
|
||||
recipientDropdownExpanded = !recipientDropdownExpanded;
|
||||
updateRecipientDropdownVisibility();
|
||||
}).dimensions(leftX, startY + 84, leftWidth, 20).build());
|
||||
|
||||
recipientCardOptionButtons.clear();
|
||||
for (int i = 0; i < MAX_RECIPIENT_OPTION_BUTTONS; i++) {
|
||||
final int index = i;
|
||||
ButtonWidget optionButton = this.addDrawableChild(ButtonWidget.builder(Text.literal("-"), button -> {
|
||||
if (index < recipientCardOptions.size()) {
|
||||
selectedRecipientCard = recipientCardOptions.get(index);
|
||||
recipientDropdownExpanded = false;
|
||||
updateRecipientDropdownVisibility();
|
||||
updateSenderCardText();
|
||||
}
|
||||
}).dimensions(leftX, startY + 108 + i * 22, leftWidth, 20).build());
|
||||
recipientCardOptionButtons.add(optionButton);
|
||||
}
|
||||
|
||||
if (!initialRecipient.isEmpty()) {
|
||||
recipientField.setText(initialRecipient);
|
||||
}
|
||||
|
||||
int arrowWidth = 36;
|
||||
int middleWidth = rightWidth - arrowWidth * 2 - 8;
|
||||
senderLeftButton = this.addDrawableChild(ButtonWidget.builder(Text.literal("<"), button -> {
|
||||
bankUiService.cycleSelectedCard(-1);
|
||||
updateSenderCardSelector();
|
||||
updateSenderCardText();
|
||||
}).dimensions(rightX, startY, arrowWidth, 20).build());
|
||||
|
||||
senderCardLabelButton = this.addDrawableChild(ButtonWidget.builder(Text.literal("00000: 0 АР"), button -> {
|
||||
}).dimensions(rightX + arrowWidth + 4, startY, middleWidth, 20).build());
|
||||
senderCardLabelButton.active = false;
|
||||
|
||||
senderRightButton = this.addDrawableChild(ButtonWidget.builder(Text.literal(">"), button -> {
|
||||
bankUiService.cycleSelectedCard(1);
|
||||
updateSenderCardSelector();
|
||||
updateSenderCardText();
|
||||
}).dimensions(rightX + arrowWidth + 4 + middleWidth + 4, startY, arrowWidth, 20).build());
|
||||
|
||||
transferButton = this.addDrawableChild(ButtonWidget.builder(Text.literal("Перевести"), button -> submit())
|
||||
.dimensions(rightX, startY + 28, rightWidth, 20)
|
||||
.build());
|
||||
|
||||
backButton = this.addDrawableChild(ButtonWidget.builder(Text.literal("Назад"), button -> this.close())
|
||||
.dimensions(rightX, startY + 56, rightWidth, 20)
|
||||
.build());
|
||||
|
||||
updateSenderCardSelector();
|
||||
updateRecipientDropdownVisibility();
|
||||
updateSenderCardText();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
if (this.client != null) {
|
||||
this.client.setScreen(parent);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void tick() {
|
||||
super.tick();
|
||||
if (pendingRecipientLookup.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
long elapsed = System.currentTimeMillis() - pendingRecipientLookupAt;
|
||||
if (elapsed < RECIPIENT_LOOKUP_DEBOUNCE_MS) {
|
||||
return;
|
||||
}
|
||||
|
||||
String lookupUsername = pendingRecipientLookup;
|
||||
pendingRecipientLookup = "";
|
||||
requestRecipientCards(lookupUsername);
|
||||
}
|
||||
|
||||
@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 = this.height / 2 - 82;
|
||||
int leftX = centerX - 210;
|
||||
int rightX = centerX + 14;
|
||||
|
||||
context.drawCenteredTextWithShadow(this.textRenderer, this.title, centerX, 20, 0xFFFFFF);
|
||||
context.drawTextWithShadow(this.textRenderer, Text.literal("Левая колонка: ввод"), leftX, startY - 18, 0xBFBFBF);
|
||||
context.drawTextWithShadow(this.textRenderer, Text.literal("Правая колонка: действия"), rightX, startY - 18, 0xBFBFBF);
|
||||
context.drawTextWithShadow(this.textRenderer, Text.literal("Сумма:"), leftX, startY - 10, 0xCCCCCC);
|
||||
context.drawTextWithShadow(this.textRenderer, Text.literal("Получатель:"), leftX, startY + 18, 0xCCCCCC);
|
||||
context.drawTextWithShadow(this.textRenderer, Text.literal("Комментарий:"), leftX, startY + 46, 0xCCCCCC);
|
||||
|
||||
if (recipientCardDropdownButton.visible) {
|
||||
context.drawTextWithShadow(this.textRenderer, Text.literal("Карта игрока"), leftX, startY + 74, 0xD7B2FF);
|
||||
}
|
||||
|
||||
context.drawCenteredTextWithShadow(this.textRenderer, senderCardText, centerX, this.height - 20, 0xA9E5A9);
|
||||
notifications.render(context, this.textRenderer, this.width, this.height);
|
||||
}
|
||||
|
||||
private void submit() {
|
||||
CardViewModel selectedCard = bankUiService.getSelectedCard();
|
||||
if (selectedCard == null) {
|
||||
notifications.show(Text.literal("Нет выбранной карты отправителя"));
|
||||
return;
|
||||
}
|
||||
|
||||
long amount;
|
||||
try {
|
||||
amount = Long.parseLong(amountField.getText().trim());
|
||||
if (amount <= 0) {
|
||||
notifications.show(Text.literal("Сумма должна быть больше 0"));
|
||||
return;
|
||||
}
|
||||
} catch (NumberFormatException exception) {
|
||||
notifications.show(Text.literal("Некорректная сумма"));
|
||||
return;
|
||||
}
|
||||
|
||||
String recipientInput = recipientField.getText().trim();
|
||||
if (!isValidRecipient(recipientInput)) {
|
||||
notifications.show(Text.literal("Укажи ник или 5 цифр карты"));
|
||||
return;
|
||||
}
|
||||
|
||||
String receiver = recipientInput;
|
||||
if (isNickname(recipientInput)) {
|
||||
if (selectedRecipientCard.isEmpty()) {
|
||||
notifications.show(Text.literal("Выбери карту получателя из списка"));
|
||||
return;
|
||||
}
|
||||
receiver = selectedRecipientCard;
|
||||
}
|
||||
|
||||
PaymentDraft draft = new PaymentDraft(
|
||||
selectedCard.id(),
|
||||
receiver,
|
||||
amount,
|
||||
commentField.getText().trim()
|
||||
);
|
||||
|
||||
boolean accepted = bankUiService.submitPayment(draft);
|
||||
String serviceMessage = bankUiService.getLastMessage();
|
||||
if (!serviceMessage.isBlank()) {
|
||||
|
||||
notifications.showMessage(serviceMessage);
|
||||
} else {
|
||||
notifications.show(accepted
|
||||
? Text.literal("Перевод выполнен")
|
||||
: Text.literal("Перевод отклонен"));
|
||||
}
|
||||
updateSenderCardSelector();
|
||||
updateSenderCardText();
|
||||
}
|
||||
|
||||
private boolean isValidRecipient(String recipient) {
|
||||
if (recipient.matches("\\d{5}")) {
|
||||
return true;
|
||||
}
|
||||
return recipient.matches("[A-Za-z0-9_]{3,16}");
|
||||
}
|
||||
|
||||
private void updateSenderCardText() {
|
||||
CardViewModel selectedCard = bankUiService.getSelectedCard();
|
||||
if (selectedCard == null) {
|
||||
senderCardText = Text.literal("Карта отправителя: не выбрана");
|
||||
return;
|
||||
}
|
||||
|
||||
String recipient = recipientField == null ? "" : recipientField.getText().trim();
|
||||
String suffix;
|
||||
if (isNickname(recipient)) {
|
||||
String recipientCard = selectedRecipientCard.isEmpty() ? "карта не выбрана" : selectedRecipientCard;
|
||||
suffix = " (ник: выбранная карта -> " + recipientCard + ")";
|
||||
} else {
|
||||
suffix = " (режим: перевод по номеру карты)";
|
||||
}
|
||||
|
||||
senderCardText = Text.literal(
|
||||
"Карта отправителя: " + selectedCard.title() + " | Баланс: " + selectedCard.balance() + suffix
|
||||
);
|
||||
}
|
||||
|
||||
private void updateSenderCardSelector() {
|
||||
CardViewModel selectedCard = bankUiService.getSelectedCard();
|
||||
if (selectedCard == null) {
|
||||
senderCardLabelButton.setMessage(Text.literal("\"00000\": \"0\" АР"));
|
||||
senderLeftButton.active = false;
|
||||
senderRightButton.active = false;
|
||||
return;
|
||||
}
|
||||
|
||||
senderLeftButton.active = true;
|
||||
senderRightButton.active = true;
|
||||
String cardId = extractCardId(selectedCard.title());
|
||||
String balance = Long.toString(selectedCard.balance());
|
||||
senderCardLabelButton.setMessage(Text.literal("\"" + cardId + "\": \"" + balance + "\" АР"));
|
||||
senderCardLabelButton.active = false;
|
||||
}
|
||||
|
||||
private void updateRecipientDropdownVisibility() {
|
||||
if (recipientCardDropdownButton == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
boolean nicknameMode = isNickname(recipientField == null ? "" : recipientField.getText().trim());
|
||||
|
||||
boolean hasRecipientCards = !recipientCardOptions.isEmpty();
|
||||
recipientCardDropdownButton.visible = nicknameMode;
|
||||
recipientCardDropdownButton.active = nicknameMode && hasRecipientCards;
|
||||
|
||||
if (!nicknameMode) {
|
||||
recipientDropdownExpanded = false;
|
||||
selectedRecipientCard = "";
|
||||
} else if (hasRecipientCards && selectedRecipientCard.isEmpty()) {
|
||||
selectedRecipientCard = recipientCardOptions.get(0);
|
||||
}
|
||||
|
||||
String dropdownText;
|
||||
if (!nicknameMode) {
|
||||
dropdownText = "Карта игрока: -";
|
||||
} else if (!hasRecipientCards) {
|
||||
dropdownText = "Карта игрока: нет карт";
|
||||
} else {
|
||||
dropdownText = "Карта игрока: " + selectedRecipientCard;
|
||||
}
|
||||
recipientCardDropdownButton.setMessage(Text.literal(dropdownText));
|
||||
|
||||
for (int i = 0; i < recipientCardOptionButtons.size(); i++) {
|
||||
ButtonWidget optionButton = recipientCardOptionButtons.get(i);
|
||||
boolean showOption = nicknameMode && recipientDropdownExpanded && i < recipientCardOptions.size();
|
||||
optionButton.visible = showOption;
|
||||
optionButton.active = showOption;
|
||||
if (i < recipientCardOptions.size()) {
|
||||
optionButton.setMessage(Text.literal(recipientCardOptions.get(i)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isNickname(String value) {
|
||||
return value.matches("[A-Za-z0-9_]{3,16}") && !value.matches("\\d+");
|
||||
}
|
||||
|
||||
private String extractCardId(String source) {
|
||||
Matcher matcher = CARD_ID_PATTERN.matcher(source);
|
||||
if (matcher.find()) {
|
||||
return matcher.group();
|
||||
}
|
||||
return "00000";
|
||||
}
|
||||
|
||||
private void requestRecipientCards(String username) {
|
||||
if (username.equals(lastRecipientLookup)) {
|
||||
return;
|
||||
}
|
||||
lastRecipientLookup = username;
|
||||
|
||||
CompletableFuture
|
||||
.supplyAsync(() -> bankUiService.loadRecipientCards(username))
|
||||
.thenAccept(cards -> {
|
||||
if (this.client == null) {
|
||||
return;
|
||||
}
|
||||
this.client.execute(() -> {
|
||||
if (recipientField == null || !username.equals(recipientField.getText().trim())) {
|
||||
return;
|
||||
}
|
||||
|
||||
recipientCardOptions.clear();
|
||||
recipientCardOptions.addAll(cards);
|
||||
selectedRecipientCard = recipientCardOptions.isEmpty() ? "" : recipientCardOptions.get(0);
|
||||
recipientDropdownExpanded = false;
|
||||
|
||||
String serviceMessage = bankUiService.getLastMessage();
|
||||
if (!serviceMessage.isBlank()) {
|
||||
notifications.showMessage(serviceMessage);
|
||||
}
|
||||
|
||||
updateRecipientDropdownVisibility();
|
||||
updateSenderCardText();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private void scheduleRecipientLookup(String username) {
|
||||
pendingRecipientLookup = username;
|
||||
pendingRecipientLookupAt = System.currentTimeMillis();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
package git.yawaflua.tech.spmega.client.ui;
|
||||
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParser;
|
||||
import com.google.gson.Strictness;
|
||||
import com.google.gson.stream.JsonReader;
|
||||
import net.minecraft.client.font.TextRenderer;
|
||||
import net.minecraft.client.gui.DrawContext;
|
||||
import net.minecraft.text.Text;
|
||||
|
||||
import java.awt.*;
|
||||
|
||||
public final class UiNotifications {
|
||||
private static final long DEFAULT_DURATION_MS = 3500L;
|
||||
private static final UiNotifications INSTANCE = new UiNotifications();
|
||||
|
||||
private Text currentText = Text.empty();
|
||||
private long visibleUntilMs;
|
||||
|
||||
private UiNotifications() {
|
||||
}
|
||||
|
||||
public static UiNotifications instance() {
|
||||
return INSTANCE;
|
||||
}
|
||||
|
||||
public static String extractMessage(String raw) {
|
||||
if (raw == null || raw.isBlank()) {
|
||||
return "";
|
||||
}
|
||||
|
||||
try {
|
||||
System.out.println(raw);
|
||||
var reader = new JsonReader(new java.io.StringReader(raw));
|
||||
reader.setStrictness(Strictness.LENIENT);
|
||||
|
||||
JsonElement parsed = JsonParser.parseReader(reader);
|
||||
JsonObject object = parsed.isJsonArray() ? parsed.getAsJsonArray().get(0).getAsJsonObject() : parsed.getAsJsonObject();
|
||||
if (object.has("message") && !object.get("message").isJsonNull()) {
|
||||
return object.get("message").getAsString();
|
||||
}
|
||||
|
||||
} catch (Exception ignored) {
|
||||
System.out.println(ignored.getMessage());
|
||||
// fallback to raw text
|
||||
}
|
||||
|
||||
return raw;
|
||||
}
|
||||
|
||||
public synchronized void show(Text text) {
|
||||
if (text == null || text.getString().isBlank()) {
|
||||
return;
|
||||
}
|
||||
currentText = text;
|
||||
visibleUntilMs = System.currentTimeMillis() + DEFAULT_DURATION_MS;
|
||||
}
|
||||
|
||||
public synchronized void showMessage(String message) {
|
||||
if (message == null || message.isBlank()) {
|
||||
return;
|
||||
}
|
||||
show(Text.literal(extractMessage(message)));
|
||||
}
|
||||
|
||||
public synchronized void render(DrawContext context, TextRenderer textRenderer, int width, int height) {
|
||||
if (currentText == null || currentText.getString().isBlank()) {
|
||||
return;
|
||||
}
|
||||
if (System.currentTimeMillis() > visibleUntilMs) {
|
||||
return;
|
||||
}
|
||||
|
||||
String message = currentText.getString();
|
||||
int padding = 6;
|
||||
int textWidth = textRenderer.getWidth(message);
|
||||
int boxWidth = textWidth + padding * 2;
|
||||
int boxHeight = textRenderer.fontHeight + padding * 2;
|
||||
int x = width - boxWidth - 10;
|
||||
int y = height - boxHeight - 10;
|
||||
|
||||
context.fill(x, y, x + boxWidth, y + boxHeight, Color.GRAY.getRGB());
|
||||
context.drawTextWithShadow(textRenderer, currentText, x + padding, y + padding, Color.WHITE.getRGB());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
package git.yawaflua.tech.spmega.client.ui;
|
||||
|
||||
import net.minecraft.client.MinecraftClient;
|
||||
import net.minecraft.client.gui.screen.Screen;
|
||||
|
||||
public final class UiOpeners {
|
||||
private UiOpeners() {
|
||||
}
|
||||
|
||||
public static void openMainMenu(MinecraftClient client) {
|
||||
if (client == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
Screen current = client.currentScreen;
|
||||
client.setScreen(new MainBankScreen(current));
|
||||
}
|
||||
|
||||
public static void openPaymentMenu(MinecraftClient client, String recipient) {
|
||||
if (client == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
Screen current = client.currentScreen;
|
||||
client.setScreen(new PaymentScreen(current, recipient));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,173 @@
|
||||
package git.yawaflua.tech.spmega.client.ui.service;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.sql.*;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public final class BankDatabase {
|
||||
private final String jdbcUrl;
|
||||
|
||||
public BankDatabase(Path databasePath) {
|
||||
this.jdbcUrl = "jdbc:sqlite:" + databasePath.toAbsolutePath();
|
||||
}
|
||||
|
||||
public void initialize() {
|
||||
try (Connection connection = open()) {
|
||||
try (Statement statement = connection.createStatement()) {
|
||||
statement.executeUpdate("""
|
||||
CREATE TABLE IF NOT EXISTS cards (
|
||||
card_id TEXT PRIMARY KEY,
|
||||
card_token TEXT NOT NULL,
|
||||
card_name TEXT,
|
||||
card_number TEXT,
|
||||
balance INTEGER NOT NULL DEFAULT 0,
|
||||
owner_uuid TEXT,
|
||||
updated_at TEXT DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
""");
|
||||
statement.executeUpdate("""
|
||||
CREATE TABLE IF NOT EXISTS transfer_history (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
sender_card_id TEXT NOT NULL,
|
||||
receiver TEXT NOT NULL,
|
||||
amount INTEGER NOT NULL,
|
||||
comment TEXT,
|
||||
balance_after INTEGER,
|
||||
status TEXT NOT NULL,
|
||||
created_at TEXT DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
""");
|
||||
}
|
||||
} catch (SQLException exception) {
|
||||
throw new RuntimeException("Failed to initialize sqlite database", exception);
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized void upsertCardCredentials(String cardId, String cardToken) {
|
||||
String sql = """
|
||||
INSERT INTO cards(card_id, card_token, updated_at)
|
||||
VALUES(?, ?, CURRENT_TIMESTAMP)
|
||||
ON CONFLICT(card_id) DO UPDATE SET
|
||||
card_token = excluded.card_token,
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
""";
|
||||
try (Connection connection = open(); PreparedStatement statement = connection.prepareStatement(sql)) {
|
||||
statement.setString(1, cardId);
|
||||
statement.setString(2, cardToken);
|
||||
statement.executeUpdate();
|
||||
} catch (SQLException exception) {
|
||||
throw new RuntimeException("Failed to upsert card credentials", exception);
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized void updateCardMeta(String cardId, String cardName, String cardNumber, long balance, String ownerUuid) {
|
||||
String sql = """
|
||||
UPDATE cards
|
||||
SET card_name = ?, card_number = ?, balance = ?, owner_uuid = ?, updated_at = CURRENT_TIMESTAMP
|
||||
WHERE card_id = ?
|
||||
""";
|
||||
try (Connection connection = open(); PreparedStatement statement = connection.prepareStatement(sql)) {
|
||||
statement.setString(1, cardName);
|
||||
statement.setString(2, cardNumber);
|
||||
statement.setLong(3, balance);
|
||||
statement.setString(4, ownerUuid);
|
||||
statement.setString(5, cardId);
|
||||
statement.executeUpdate();
|
||||
} catch (SQLException exception) {
|
||||
throw new RuntimeException("Failed to update card meta", exception);
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized void updateCardBalance(String cardId, long balance) {
|
||||
String sql = "UPDATE cards SET balance = ?, updated_at = CURRENT_TIMESTAMP WHERE card_id = ?";
|
||||
try (Connection connection = open(); PreparedStatement statement = connection.prepareStatement(sql)) {
|
||||
statement.setLong(1, balance);
|
||||
statement.setString(2, cardId);
|
||||
statement.executeUpdate();
|
||||
} catch (SQLException exception) {
|
||||
throw new RuntimeException("Failed to update card balance", exception);
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized void deleteCard(String cardId) {
|
||||
String sql = "DELETE FROM cards WHERE card_id = ?";
|
||||
try (Connection connection = open(); PreparedStatement statement = connection.prepareStatement(sql)) {
|
||||
statement.setString(1, cardId);
|
||||
statement.executeUpdate();
|
||||
} catch (SQLException exception) {
|
||||
throw new RuntimeException("Failed to delete card", exception);
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized List<StoredCard> loadCards() {
|
||||
String sql = "SELECT card_id, card_token, card_name, card_number, balance, owner_uuid FROM cards ORDER BY updated_at DESC";
|
||||
List<StoredCard> result = new ArrayList<>();
|
||||
try (Connection connection = open();
|
||||
PreparedStatement statement = connection.prepareStatement(sql);
|
||||
ResultSet rs = statement.executeQuery()) {
|
||||
while (rs.next()) {
|
||||
result.add(new StoredCard(
|
||||
rs.getString("card_id"),
|
||||
rs.getString("card_token"),
|
||||
rs.getString("card_name"),
|
||||
rs.getString("card_number"),
|
||||
rs.getLong("balance"),
|
||||
rs.getString("owner_uuid")
|
||||
));
|
||||
}
|
||||
return result;
|
||||
} catch (SQLException exception) {
|
||||
throw new RuntimeException("Failed to load cards", exception);
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized CardCredentials getCredentials(String cardId) {
|
||||
String sql = "SELECT card_id, card_token FROM cards WHERE card_id = ?";
|
||||
try (Connection connection = open(); PreparedStatement statement = connection.prepareStatement(sql)) {
|
||||
statement.setString(1, cardId);
|
||||
try (ResultSet rs = statement.executeQuery()) {
|
||||
if (rs.next()) {
|
||||
return new CardCredentials(rs.getString("card_id"), rs.getString("card_token"));
|
||||
}
|
||||
}
|
||||
return null;
|
||||
} catch (SQLException exception) {
|
||||
throw new RuntimeException("Failed to get card credentials", exception);
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized void insertTransferHistory(
|
||||
String senderCardId,
|
||||
String receiver,
|
||||
long amount,
|
||||
String comment,
|
||||
Long balanceAfter,
|
||||
String status
|
||||
) {
|
||||
String sql = """
|
||||
INSERT INTO transfer_history(sender_card_id, receiver, amount, comment, balance_after, status)
|
||||
VALUES(?, ?, ?, ?, ?, ?)
|
||||
""";
|
||||
try (Connection connection = open(); PreparedStatement statement = connection.prepareStatement(sql)) {
|
||||
statement.setString(1, senderCardId);
|
||||
statement.setString(2, receiver);
|
||||
statement.setLong(3, amount);
|
||||
statement.setString(4, comment);
|
||||
if (balanceAfter == null) {
|
||||
statement.setNull(5, java.sql.Types.INTEGER);
|
||||
} else {
|
||||
statement.setLong(5, balanceAfter);
|
||||
}
|
||||
statement.setString(6, status);
|
||||
statement.executeUpdate();
|
||||
} catch (SQLException exception) {
|
||||
throw new RuntimeException("Failed to insert transfer history", exception);
|
||||
}
|
||||
}
|
||||
|
||||
private Connection open() throws SQLException {
|
||||
return DriverManager.getConnection(jdbcUrl);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,298 @@
|
||||
package git.yawaflua.tech.spmega.client.ui.service;
|
||||
|
||||
import git.yawaflua.tech.spmega.api.SPWorldsApiClient;
|
||||
import net.fabricmc.loader.api.FabricLoader;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
public final class BankUiService {
|
||||
private static final BankUiService INSTANCE = new BankUiService();
|
||||
|
||||
private final List<CardViewModel> cards = new ArrayList<>();
|
||||
private final BankDatabase database;
|
||||
private final SPWorldsApiClient apiClient;
|
||||
private final String apiDomain = "https://spworlds.ru";
|
||||
|
||||
private int selectedCardIndex;
|
||||
private String lastMessage = "";
|
||||
|
||||
private BankUiService() {
|
||||
this.database = new BankDatabase(FabricLoader.getInstance().getConfigDir().resolve("spmega.db"));
|
||||
this.database.initialize();
|
||||
this.apiClient = new SPWorldsApiClient(apiDomain);
|
||||
|
||||
reloadCardsFromDb();
|
||||
}
|
||||
|
||||
public static BankUiService instance() {
|
||||
return INSTANCE;
|
||||
}
|
||||
|
||||
private static SPWorldsApiClient.CardAuth toApiAuth(CardCredentials credentials) {
|
||||
return new SPWorldsApiClient.CardAuth(credentials.cardId(), credentials.cardToken());
|
||||
}
|
||||
|
||||
private static String trimMessage(String message) {
|
||||
if (message == null) {
|
||||
return "unknown";
|
||||
}
|
||||
return message.length() > 120 ? message.substring(0, 120) : message;
|
||||
}
|
||||
|
||||
private static String normalizeUuid(String rawUuid) {
|
||||
return rawUuid == null ? "" : rawUuid.toLowerCase(Locale.ROOT).replace("-", "");
|
||||
}
|
||||
|
||||
private static String extractLastDigits(String value) {
|
||||
String normalized = value == null ? "" : value.replace("-", "");
|
||||
if (normalized.length() < 5) {
|
||||
return "00000";
|
||||
}
|
||||
return normalized.substring(normalized.length() - 5);
|
||||
}
|
||||
|
||||
public synchronized List<CardViewModel> getCards() {
|
||||
return Collections.unmodifiableList(cards);
|
||||
}
|
||||
|
||||
public synchronized CardViewModel getSelectedCard() {
|
||||
if (cards.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
selectedCardIndex = Math.floorMod(selectedCardIndex, cards.size());
|
||||
return cards.get(selectedCardIndex);
|
||||
}
|
||||
|
||||
public synchronized int getSelectedCardIndex() {
|
||||
if (cards.isEmpty()) {
|
||||
return 0;
|
||||
}
|
||||
selectedCardIndex = Math.floorMod(selectedCardIndex, cards.size());
|
||||
return selectedCardIndex;
|
||||
}
|
||||
|
||||
public synchronized void setSelectedCardIndex(int index) {
|
||||
if (cards.isEmpty()) {
|
||||
selectedCardIndex = 0;
|
||||
return;
|
||||
}
|
||||
selectedCardIndex = Math.floorMod(index, cards.size());
|
||||
}
|
||||
|
||||
public synchronized CardViewModel cycleSelectedCard(int direction) {
|
||||
if (cards.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
selectedCardIndex = Math.floorMod(selectedCardIndex + direction, cards.size());
|
||||
return cards.get(selectedCardIndex);
|
||||
}
|
||||
|
||||
public synchronized String getLastMessage() {
|
||||
return lastMessage;
|
||||
}
|
||||
|
||||
public synchronized void refreshOnServerJoin(String playerUuid) {
|
||||
List<StoredCard> storedCards = database.loadCards();
|
||||
for (StoredCard card : storedCards) {
|
||||
refreshCard(card.cardId(), card.cardToken(), playerUuid, false, false);
|
||||
}
|
||||
reloadCardsFromDb();
|
||||
}
|
||||
|
||||
public synchronized List<String> loadRecipientCards(String username) {
|
||||
CardCredentials credentials = getSelectedCredentials();
|
||||
if (credentials == null || username == null || username.isBlank()) {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
try {
|
||||
List<SPWorldsApiClient.PlayerCard> apiCards = apiClient.getPlayerCards(username, toApiAuth(credentials));
|
||||
List<String> numbers = new ArrayList<>();
|
||||
for (SPWorldsApiClient.PlayerCard apiCard : apiCards) {
|
||||
if (apiCard.number() != null && !apiCard.number().isBlank()) {
|
||||
numbers.add(apiCard.number());
|
||||
}
|
||||
}
|
||||
lastMessage = "";
|
||||
return numbers;
|
||||
} catch (Exception exception) {
|
||||
lastMessage = "Не удалось получить карты игрока: " + exception.getMessage();
|
||||
return List.of();
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized String addCard(String cardIdRaw, String cardTokenRaw, String playerUuid) {
|
||||
String cardId = cardIdRaw == null ? "" : cardIdRaw.trim();
|
||||
String cardToken = cardTokenRaw == null ? "" : cardTokenRaw.trim();
|
||||
|
||||
if (cardId.isEmpty() || cardToken.isEmpty()) {
|
||||
lastMessage = "Укажи cardId и cardToken";
|
||||
return lastMessage;
|
||||
}
|
||||
|
||||
try {
|
||||
UUID.fromString(cardId);
|
||||
} catch (IllegalArgumentException exception) {
|
||||
lastMessage = "cardId должен быть UUID";
|
||||
return lastMessage;
|
||||
}
|
||||
|
||||
database.upsertCardCredentials(cardId, cardToken);
|
||||
boolean refreshed = refreshCard(cardId, cardToken, playerUuid, true, true);
|
||||
if (!refreshed) {
|
||||
database.deleteCard(cardId);
|
||||
}
|
||||
reloadCardsFromDb();
|
||||
|
||||
if (refreshed && lastMessage.isBlank()) {
|
||||
lastMessage = "Карта добавлена";
|
||||
}
|
||||
return lastMessage;
|
||||
}
|
||||
|
||||
public synchronized void removeSelectedCard() {
|
||||
CardViewModel selected = getSelectedCard();
|
||||
if (selected == null) {
|
||||
lastMessage = "Нет карты для удаления";
|
||||
return;
|
||||
}
|
||||
|
||||
database.deleteCard(selected.id());
|
||||
reloadCardsFromDb();
|
||||
if (selectedCardIndex >= cards.size()) {
|
||||
selectedCardIndex = Math.max(0, cards.size() - 1);
|
||||
}
|
||||
lastMessage = "Карта удалена";
|
||||
}
|
||||
|
||||
public synchronized void refreshSelectedCard(String playerUuid) {
|
||||
CardViewModel selected = getSelectedCard();
|
||||
if (selected == null) {
|
||||
lastMessage = "Нет карты для обновления";
|
||||
return;
|
||||
}
|
||||
|
||||
CardCredentials credentials = database.getCredentials(selected.id());
|
||||
if (credentials == null) {
|
||||
lastMessage = "Не найдены креды карты";
|
||||
return;
|
||||
}
|
||||
|
||||
refreshCard(credentials.cardId(), credentials.cardToken(), playerUuid, false, false);
|
||||
reloadCardsFromDb();
|
||||
}
|
||||
|
||||
public synchronized boolean submitPayment(PaymentDraft draft) {
|
||||
CardCredentials credentials = database.getCredentials(draft.senderCardId());
|
||||
if (credentials == null) {
|
||||
lastMessage = "Не найдены креды карты отправителя";
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
SPWorldsApiClient.TransactionResult result = apiClient.createTransaction(
|
||||
toApiAuth(credentials),
|
||||
draft.recipient(),
|
||||
draft.amount(),
|
||||
draft.comment()
|
||||
);
|
||||
|
||||
database.updateCardBalance(credentials.cardId(), result.balance());
|
||||
database.insertTransferHistory(
|
||||
credentials.cardId(),
|
||||
draft.recipient(),
|
||||
draft.amount(),
|
||||
draft.comment(),
|
||||
result.balance(),
|
||||
"SUCCESS"
|
||||
);
|
||||
|
||||
reloadCardsFromDb();
|
||||
lastMessage = "Перевод выполнен";
|
||||
return true;
|
||||
} catch (Exception exception) {
|
||||
database.insertTransferHistory(
|
||||
credentials.cardId(),
|
||||
draft.recipient(),
|
||||
draft.amount(),
|
||||
draft.comment(),
|
||||
null,
|
||||
"FAILED: " + trimMessage(exception.getMessage())
|
||||
);
|
||||
lastMessage = "Ошибка перевода: " + exception.getMessage();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean refreshCard(
|
||||
String cardId,
|
||||
String cardToken,
|
||||
String playerUuid,
|
||||
boolean reportOwnerWarning,
|
||||
boolean requireCardInAccount
|
||||
) {
|
||||
try {
|
||||
SPWorldsApiClient.CardAuth auth = new SPWorldsApiClient.CardAuth(cardId, cardToken);
|
||||
SPWorldsApiClient.AccountMe me = apiClient.getAccountMe(auth);
|
||||
SPWorldsApiClient.CardInfo cardInfo = apiClient.getCardInfo(auth);
|
||||
|
||||
SPWorldsApiClient.AccountCard currentCard = me.cards().stream()
|
||||
.filter(card -> Objects.equals(card.id(), cardId))
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
|
||||
if (requireCardInAccount && currentCard == null) {
|
||||
lastMessage = "Карта не найдена в аккаунте SPWorlds";
|
||||
return false;
|
||||
}
|
||||
|
||||
String cardName = currentCard == null || currentCard.name().isBlank() ? "Карта" : currentCard.name();
|
||||
String cardNumber = currentCard == null || currentCard.number().isBlank() ? extractLastDigits(cardId) : currentCard.number();
|
||||
String ownerUuid = normalizeUuid(me.minecraftUuid());
|
||||
|
||||
database.updateCardMeta(cardId, cardName, cardNumber, cardInfo.balance(), ownerUuid);
|
||||
|
||||
if (reportOwnerWarning && playerUuid != null && !playerUuid.isBlank()) {
|
||||
String normalizedPlayerUuid = normalizeUuid(playerUuid);
|
||||
if (!normalizedPlayerUuid.equals(ownerUuid)) {
|
||||
lastMessage = "Вы не владелец карты. Часть функций может быть ограничена.";
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
lastMessage = "";
|
||||
return true;
|
||||
} catch (Exception exception) {
|
||||
lastMessage = "Не удалось обновить карту: " + exception.getMessage();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private void reloadCardsFromDb() {
|
||||
List<StoredCard> storedCards = database.loadCards();
|
||||
cards.clear();
|
||||
|
||||
for (StoredCard stored : storedCards) {
|
||||
String cardNumber = stored.cardNumber() == null || stored.cardNumber().isBlank()
|
||||
? extractLastDigits(stored.cardId())
|
||||
: stored.cardNumber();
|
||||
String cardName = stored.cardName() == null || stored.cardName().isBlank() ? "Карта" : stored.cardName();
|
||||
String title = cardNumber + ": " + cardName;
|
||||
cards.add(new CardViewModel(stored.cardId(), title, stored.balance()));
|
||||
}
|
||||
|
||||
if (cards.isEmpty()) {
|
||||
selectedCardIndex = 0;
|
||||
} else {
|
||||
selectedCardIndex = Math.floorMod(selectedCardIndex, cards.size());
|
||||
}
|
||||
}
|
||||
|
||||
private CardCredentials getSelectedCredentials() {
|
||||
CardViewModel selected = getSelectedCard();
|
||||
if (selected == null) {
|
||||
return null;
|
||||
}
|
||||
return database.getCredentials(selected.id());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package git.yawaflua.tech.spmega.client.ui.service;
|
||||
|
||||
public record CardCredentials(String cardId, String cardToken) {
|
||||
}
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
package git.yawaflua.tech.spmega.client.ui.service;
|
||||
|
||||
public record CardViewModel(String id, String title, long balance) {
|
||||
}
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
package git.yawaflua.tech.spmega.client.ui.service;
|
||||
|
||||
public record PaymentDraft(String senderCardId, String recipient, long amount, String comment) {
|
||||
}
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
package git.yawaflua.tech.spmega.client.ui.service;
|
||||
|
||||
public record StoredCard(String cardId, String cardToken, String cardName, String cardNumber, long balance,
|
||||
String ownerUuid) {
|
||||
}
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
package git.yawaflua.tech.spmega.mixin.client;
|
||||
|
||||
import git.yawaflua.tech.spmega.client.ui.UiOpeners;
|
||||
import net.minecraft.client.gui.screen.GameMenuScreen;
|
||||
import net.minecraft.client.gui.screen.Screen;
|
||||
import net.minecraft.client.gui.widget.ButtonWidget;
|
||||
import net.minecraft.text.Text;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
import org.spongepowered.asm.mixin.injection.Inject;
|
||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
|
||||
|
||||
@Mixin(GameMenuScreen.class)
|
||||
public abstract class GameMenuScreenMixin extends Screen {
|
||||
protected GameMenuScreenMixin(Text title) {
|
||||
super(title);
|
||||
}
|
||||
|
||||
@Inject(method = "init", at = @At("TAIL"))
|
||||
private void spmega$addOpenMenuButton(CallbackInfo ci) {
|
||||
int buttonWidth = 96;
|
||||
int x = this.width / 2 + 106;
|
||||
int y = this.height / 4 + 24 - 16;
|
||||
this.addDrawableChild(ButtonWidget.builder(Text.literal("SPMega"), button -> {
|
||||
UiOpeners.openMainMenu(this.client);
|
||||
}).dimensions(x, y, buttonWidth, 20).build());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"category.spmega": "SPMega",
|
||||
"key.spmega.open_menu": "Open SPMega Menu"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"required": true,
|
||||
"minVersion": "0.8",
|
||||
"package": "git.yawaflua.tech.spmega.mixin.client",
|
||||
"compatibilityLevel": "JAVA_21",
|
||||
"client": [
|
||||
"GameMenuScreenMixin"
|
||||
],
|
||||
"injectors": {
|
||||
"defaultRequire": 1
|
||||
},
|
||||
"overwrites": {
|
||||
"requireAnnotations": true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
package git.yawaflua.tech.spmega;
|
||||
|
||||
import net.fabricmc.loader.api.FabricLoader;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Properties;
|
||||
import java.util.UUID;
|
||||
|
||||
public final class ConfigManager {
|
||||
private static final String FILE_NAME = "spmega.properties";
|
||||
|
||||
private ConfigManager() {
|
||||
}
|
||||
|
||||
public static ModConfig loadOrCreate() {
|
||||
Path configPath = FabricLoader.getInstance().getConfigDir().resolve(FILE_NAME);
|
||||
Properties properties = new Properties();
|
||||
ModConfig defaults = ModConfig.createDefault();
|
||||
boolean shouldSave = false;
|
||||
|
||||
if (Files.exists(configPath)) {
|
||||
try (InputStream inputStream = Files.newInputStream(configPath)) {
|
||||
properties.load(inputStream);
|
||||
} catch (IOException exception) {
|
||||
shouldSave = true;
|
||||
}
|
||||
} else {
|
||||
shouldSave = true;
|
||||
}
|
||||
|
||||
String apiDomain = readString(properties, "api.domain", defaults.apiDomain());
|
||||
if (!apiDomain.equals(properties.getProperty("api.domain"))) {
|
||||
shouldSave = true;
|
||||
}
|
||||
|
||||
String apiToken = readString(properties, "api.token", defaults.apiToken());
|
||||
if (!apiToken.equals(properties.getProperty("api.token"))) {
|
||||
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);
|
||||
|
||||
if (shouldSave) {
|
||||
save(configPath, config);
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
private static String readString(Properties properties, String key, String fallback) {
|
||||
String value = properties.getProperty(key);
|
||||
if (value == null) {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
String trimmed = value.trim();
|
||||
return trimmed.isEmpty() ? fallback : trimmed;
|
||||
}
|
||||
|
||||
private static UUID readUuid(Properties properties, String key, UUID fallback) {
|
||||
String value = properties.getProperty(key);
|
||||
if (value == null || value.trim().isEmpty()) {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
try {
|
||||
return UUID.fromString(value.trim());
|
||||
} catch (IllegalArgumentException exception) {
|
||||
// Self-heal broken UUID values by falling back to a valid default.
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean readBoolean(Properties properties, String key, boolean fallback) {
|
||||
String value = properties.getProperty(key);
|
||||
if (value == null) {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
String normalized = value.trim().toLowerCase();
|
||||
if ("true".equals(normalized)) {
|
||||
return true;
|
||||
}
|
||||
if ("false".equals(normalized)) {
|
||||
return false;
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
|
||||
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()));
|
||||
|
||||
try {
|
||||
Files.createDirectories(configPath.getParent());
|
||||
try (OutputStream outputStream = Files.newOutputStream(configPath)) {
|
||||
properties.store(outputStream, "SPMega config");
|
||||
}
|
||||
} catch (IOException exception) {
|
||||
throw new RuntimeException("Failed to save config: " + configPath, exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
package git.yawaflua.tech.spmega;
|
||||
|
||||
public record ModConfig(String apiDomain, String apiToken, boolean signQuickPayEnabled) {
|
||||
public static final String DEFAULT_API_DOMAIN = "https://spworlds.ru";
|
||||
public static final String DEFAULT_API_TOKEN = "ulBKE9MWEtIGiPAhXV69I28W9BRiSrV3";
|
||||
public static final boolean DEFAULT_SIGN_QUICK_PAY_ENABLED = true;
|
||||
|
||||
public static ModConfig createDefault() {
|
||||
return new ModConfig(
|
||||
DEFAULT_API_DOMAIN,
|
||||
DEFAULT_API_TOKEN,
|
||||
DEFAULT_SIGN_QUICK_PAY_ENABLED
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
package git.yawaflua.tech.spmega;
|
||||
|
||||
import net.fabricmc.api.ModInitializer;
|
||||
|
||||
public class SPMega implements ModInitializer {
|
||||
private static ModConfig config;
|
||||
|
||||
public static ModConfig getConfig() {
|
||||
return config;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onInitialize() {
|
||||
config = ConfigManager.loadOrCreate();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
package git.yawaflua.tech.spmega.api;
|
||||
|
||||
import com.google.gson.*;
|
||||
|
||||
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.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Base64;
|
||||
import java.util.List;
|
||||
|
||||
public final class SPWorldsApiClient {
|
||||
private final HttpClient httpClient;
|
||||
private final Gson gson;
|
||||
private final String baseUrl;
|
||||
|
||||
public SPWorldsApiClient(String baseUrl) {
|
||||
this.httpClient = HttpClient.newHttpClient();
|
||||
this.gson = new Gson();
|
||||
this.baseUrl = normalizeBaseUrl(baseUrl);
|
||||
}
|
||||
|
||||
private static String encodeAuth(CardAuth auth) {
|
||||
String raw = auth.cardId() + ":" + auth.cardToken();
|
||||
return Base64.getEncoder().encodeToString(raw.getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
private static String normalizeBaseUrl(String rawBaseUrl) {
|
||||
String fallback = "https://spworlds.ru";
|
||||
if (rawBaseUrl == null || rawBaseUrl.trim().isEmpty()) {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
String value = rawBaseUrl.trim();
|
||||
if (value.endsWith("/")) {
|
||||
return value.substring(0, value.length() - 1);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
private static String getString(JsonObject json, String key) {
|
||||
if (!json.has(key) || json.get(key).isJsonNull()) {
|
||||
return "";
|
||||
}
|
||||
return json.get(key).getAsString();
|
||||
}
|
||||
|
||||
public CardInfo getCardInfo(CardAuth auth) throws IOException, InterruptedException {
|
||||
HttpRequest request = requestBuilder("/api/public/card", auth).GET().build();
|
||||
String body = send(request);
|
||||
JsonObject json = JsonParser.parseString(body).getAsJsonObject();
|
||||
long balance = json.has("balance") ? json.get("balance").getAsLong() : 0L;
|
||||
String webhook = json.has("webhook") && !json.get("webhook").isJsonNull()
|
||||
? json.get("webhook").getAsString()
|
||||
: "";
|
||||
return new CardInfo(balance, webhook);
|
||||
}
|
||||
|
||||
public List<PlayerCard> getPlayerCards(String username, CardAuth auth) throws IOException, InterruptedException {
|
||||
HttpRequest request = requestBuilder("/api/public/accounts/" + username + "/cards", auth).GET().build();
|
||||
String body = send(request);
|
||||
JsonArray json = JsonParser.parseString(body).getAsJsonArray();
|
||||
|
||||
List<PlayerCard> cards = new ArrayList<>();
|
||||
for (JsonElement element : json) {
|
||||
JsonObject card = element.getAsJsonObject();
|
||||
String name = card.has("name") ? card.get("name").getAsString() : "";
|
||||
String number = card.has("number") ? card.get("number").getAsString() : "";
|
||||
cards.add(new PlayerCard(name, number));
|
||||
}
|
||||
return cards;
|
||||
}
|
||||
|
||||
public AccountMe getAccountMe(CardAuth auth) throws IOException, InterruptedException {
|
||||
HttpRequest request = requestBuilder("/api/public/accounts/me", auth).GET().build();
|
||||
String body = send(request);
|
||||
JsonObject json = JsonParser.parseString(body).getAsJsonObject();
|
||||
|
||||
String id = json.has("id") ? json.get("id").getAsString() : "";
|
||||
String username = json.has("username") ? json.get("username").getAsString() : "";
|
||||
String minecraftUuid = json.has("minecraftUUID") ? json.get("minecraftUUID").getAsString() : "";
|
||||
|
||||
List<AccountCard> cards = new ArrayList<>();
|
||||
if (json.has("cards") && json.get("cards").isJsonArray()) {
|
||||
for (JsonElement element : json.getAsJsonArray("cards")) {
|
||||
JsonObject card = element.getAsJsonObject();
|
||||
cards.add(new AccountCard(
|
||||
getString(card, "id"),
|
||||
getString(card, "name"),
|
||||
getString(card, "number"),
|
||||
card.has("color") && !card.get("color").isJsonNull() ? card.get("color").getAsInt() : 0
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
return new AccountMe(id, username, minecraftUuid, cards);
|
||||
}
|
||||
|
||||
public TransactionResult createTransaction(CardAuth auth, String receiver, long amount, String comment)
|
||||
throws IOException, InterruptedException {
|
||||
JsonObject payload = new JsonObject();
|
||||
payload.addProperty("receiver", receiver);
|
||||
payload.addProperty("amount", amount);
|
||||
payload.addProperty("comment", comment.isEmpty() ? "Перевод через SPMega" : comment);
|
||||
|
||||
HttpRequest request = requestBuilder("/api/public/transactions", auth)
|
||||
.POST(HttpRequest.BodyPublishers.ofString(gson.toJson(payload)))
|
||||
.build();
|
||||
|
||||
String body = send(request);
|
||||
JsonObject json = JsonParser.parseString(body).getAsJsonObject();
|
||||
long balance = json.has("balance") ? json.get("balance").getAsLong() : 0L;
|
||||
return new TransactionResult(balance);
|
||||
}
|
||||
|
||||
private HttpRequest.Builder requestBuilder(String path, CardAuth auth) {
|
||||
return HttpRequest.newBuilder(URI.create(baseUrl + path))
|
||||
.header("Authorization", "Bearer " + encodeAuth(auth))
|
||||
.header("Content-Type", "application/json")
|
||||
.header("Accept", "application/json");
|
||||
}
|
||||
|
||||
private String send(HttpRequest request) throws IOException, InterruptedException {
|
||||
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
|
||||
int statusCode = response.statusCode();
|
||||
if (statusCode < 200 || statusCode >= 300) {
|
||||
System.out.println(response.body());
|
||||
JsonElement parsed = JsonParser.parseString(response.body());
|
||||
JsonObject object = parsed.isJsonArray() ? parsed.getAsJsonArray().get(0).getAsJsonObject() : parsed.getAsJsonObject();
|
||||
var message = "";
|
||||
if (object.has("error") && !object.get("error").isJsonNull()) {
|
||||
message = "Ошибка в запросе: " + object.get("error").getAsString();
|
||||
} else if (object.has("message") && !object.get("message").isJsonNull()) {
|
||||
message = object.get("message").getAsString();
|
||||
}
|
||||
throw new IOException(statusCode + ": " + message);
|
||||
}
|
||||
return response.body();
|
||||
}
|
||||
|
||||
public record CardAuth(String cardId, String cardToken) {
|
||||
}
|
||||
|
||||
public record CardInfo(long balance, String webhook) {
|
||||
}
|
||||
|
||||
public record PlayerCard(String name, String number) {
|
||||
}
|
||||
|
||||
public record AccountMe(String id, String username, String minecraftUuid, List<AccountCard> cards) {
|
||||
}
|
||||
|
||||
public record AccountCard(String id, String name, String number, int color) {
|
||||
}
|
||||
|
||||
public record TransactionResult(long balance) {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"id": "spmega",
|
||||
"version": "${version}",
|
||||
"name": "SPMega",
|
||||
"description": "yawaflua`s SPRadar+SPMHelper mod, thats make a lot!",
|
||||
"authors": [],
|
||||
"contact": {},
|
||||
"license": "CC-BY-SA-4.0",
|
||||
"icon": "assets/spmega/icon.png",
|
||||
"environment": "client",
|
||||
"entrypoints": {
|
||||
"fabric-datagen": [
|
||||
"git.yawaflua.tech.spmega.client.SPMegaDataGenerator"
|
||||
],
|
||||
"client": [
|
||||
"git.yawaflua.tech.spmega.client.SPMegaClient"
|
||||
],
|
||||
"main": [
|
||||
"git.yawaflua.tech.spmega.SPMega"
|
||||
]
|
||||
},
|
||||
"mixins": [
|
||||
"spmega.mixins.json",
|
||||
{
|
||||
"config": "spmega.client.mixins.json",
|
||||
"environment": "client"
|
||||
}
|
||||
],
|
||||
"depends": {
|
||||
"fabricloader": ">=${loader_version}",
|
||||
"fabric-api": "*",
|
||||
"minecraft": "${minecraft_version}",
|
||||
"cloth-config2": ">=${cloth_config_version}"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"required": true,
|
||||
"minVersion": "0.8",
|
||||
"package": "git.yawaflua.tech.spmega.mixin",
|
||||
"compatibilityLevel": "JAVA_21",
|
||||
"mixins": [
|
||||
],
|
||||
"injectors": {
|
||||
"defaultRequire": 1
|
||||
},
|
||||
"overwrites": {
|
||||
"requireAnnotations": true
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user