commit 2bd90bdb50b07e50e83c568361c97b0df3ef65ea Author: TermiNexus Date: Fri Jul 24 11:36:23 2026 +0800 初始化上传 diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..20dd8f0 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,11 @@ +# +# https://help.github.com/articles/dealing-with-line-endings/ +# +# Linux start script should use lf +/gradlew text eol=lf + +# These are Windows script files and should use crlf +*.bat text eol=crlf + +/build/ +/.gradle/ \ No newline at end of file diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..524fb1b --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,30 @@ +# Automatically build the project and run any configured tests for every push +# and submitted pull request. This can help catch issues that only occur on +# certain platforms or Java versions, and provides a first line of defence +# against bad commits. + +name: build +on: [pull_request, push] + +jobs: + build: + runs-on: ubuntu-24.04 + steps: + - name: checkout repository + uses: actions/checkout@v6 + - name: validate gradle wrapper + uses: gradle/actions/wrapper-validation@v6 + - name: setup jdk + uses: actions/setup-java@v5 + with: + java-version: '25' + distribution: 'microsoft' + - name: make gradle wrapper executable + run: chmod +x ./gradlew + - name: build + run: ./gradlew build + - name: capture build artifacts + uses: actions/upload-artifact@v7 + with: + name: Artifacts + path: build/libs/ diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c476faf --- /dev/null +++ b/.gitignore @@ -0,0 +1,40 @@ +# gradle + +.gradle/ +build/ +out/ +classes/ + +# eclipse + +*.launch + +# idea + +.idea/ +*.iml +*.ipr +*.iws + +# vscode + +.settings/ +.vscode/ +bin/ +.classpath +.project + +# macos + +*.DS_Store + +# fabric + +run/ + +# java + +hs_err_*.log +replay_*.log +*.hprof +*.jfr diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..1625c17 --- /dev/null +++ b/LICENSE @@ -0,0 +1,121 @@ +Creative Commons Legal Code + +CC0 1.0 Universal + + CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE + LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN + ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS + INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES + REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS + PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM + THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED + HEREUNDER. + +Statement of Purpose + +The laws of most jurisdictions throughout the world automatically confer +exclusive Copyright and Related Rights (defined below) upon the creator +and subsequent owner(s) (each and all, an "owner") of an original work of +authorship and/or a database (each, a "Work"). + +Certain owners wish to permanently relinquish those rights to a Work for +the purpose of contributing to a commons of creative, cultural and +scientific works ("Commons") that the public can reliably and without fear +of later claims of infringement build upon, modify, incorporate in other +works, reuse and redistribute as freely as possible in any form whatsoever +and for any purposes, including without limitation commercial purposes. +These owners may contribute to the Commons to promote the ideal of a free +culture and the further production of creative, cultural and scientific +works, or to gain reputation or greater distribution for their Work in +part through the use and efforts of others. + +For these and/or other purposes and motivations, and without any +expectation of additional consideration or compensation, the person +associating CC0 with a Work (the "Affirmer"), to the extent that he or she +is an owner of Copyright and Related Rights in the Work, voluntarily +elects to apply CC0 to the Work and publicly distribute the Work under its +terms, with knowledge of his or her Copyright and Related Rights in the +Work and the meaning and intended legal effect of CC0 on those rights. + +1. Copyright and Related Rights. A Work made available under CC0 may be +protected by copyright and related or neighboring rights ("Copyright and +Related Rights"). Copyright and Related Rights include, but are not +limited to, the following: + + i. the right to reproduce, adapt, distribute, perform, display, + communicate, and translate a Work; + ii. moral rights retained by the original author(s) and/or performer(s); +iii. publicity and privacy rights pertaining to a person's image or + likeness depicted in a Work; + iv. rights protecting against unfair competition in regards to a Work, + subject to the limitations in paragraph 4(a), below; + v. rights protecting the extraction, dissemination, use and reuse of data + in a Work; + vi. database rights (such as those arising under Directive 96/9/EC of the + European Parliament and of the Council of 11 March 1996 on the legal + protection of databases, and under any national implementation + thereof, including any amended or successor version of such + directive); and +vii. other similar, equivalent or corresponding rights throughout the + world based on applicable law or treaty, and any national + implementations thereof. + +2. Waiver. To the greatest extent permitted by, but not in contravention +of, applicable law, Affirmer hereby overtly, fully, permanently, +irrevocably and unconditionally waives, abandons, and surrenders all of +Affirmer's Copyright and Related Rights and associated claims and causes +of action, whether now known or unknown (including existing as well as +future claims and causes of action), in the Work (i) in all territories +worldwide, (ii) for the maximum duration provided by applicable law or +treaty (including future time extensions), (iii) in any current or future +medium and for any number of copies, and (iv) for any purpose whatsoever, +including without limitation commercial, advertising or promotional +purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each +member of the public at large and to the detriment of Affirmer's heirs and +successors, fully intending that such Waiver shall not be subject to +revocation, rescission, cancellation, termination, or any other legal or +equitable action to disrupt the quiet enjoyment of the Work by the public +as contemplated by Affirmer's express Statement of Purpose. + +3. Public License Fallback. Should any part of the Waiver for any reason +be judged legally invalid or ineffective under applicable law, then the +Waiver shall be preserved to the maximum extent permitted taking into +account Affirmer's express Statement of Purpose. In addition, to the +extent the Waiver is so judged Affirmer hereby grants to each affected +person a royalty-free, non transferable, non sublicensable, non exclusive, +irrevocable and unconditional license to exercise Affirmer's Copyright and +Related Rights in the Work (i) in all territories worldwide, (ii) for the +maximum duration provided by applicable law or treaty (including future +time extensions), (iii) in any current or future medium and for any number +of copies, and (iv) for any purpose whatsoever, including without +limitation commercial, advertising or promotional purposes (the +"License"). The License shall be deemed effective as of the date CC0 was +applied by Affirmer to the Work. Should any part of the License for any +reason be judged legally invalid or ineffective under applicable law, such +partial invalidity or ineffectiveness shall not invalidate the remainder +of the License, and in such case Affirmer hereby affirms that he or she +will not (i) exercise any of his or her remaining Copyright and Related +Rights in the Work or (ii) assert any associated claims and causes of +action with respect to the Work, in either case contrary to Affirmer's +express Statement of Purpose. + +4. Limitations and Disclaimers. + + a. No trademark or patent rights held by Affirmer are waived, abandoned, + surrendered, licensed or otherwise affected by this document. + b. Affirmer offers the Work as-is and makes no representations or + warranties of any kind concerning the Work, express, implied, + statutory or otherwise, including without limitation warranties of + title, merchantability, fitness for a particular purpose, non + infringement, or the absence of latent or other defects, accuracy, or + the present or absence of errors, whether or not discoverable, all to + the greatest extent permissible under applicable law. + c. Affirmer disclaims responsibility for clearing rights of other persons + that may apply to the Work or any use thereof, including without + limitation any person's Copyright and Related Rights in the Work. + Further, Affirmer disclaims responsibility for obtaining any necessary + consents, permissions or other rights required for any use of the + Work. + d. Affirmer understands and acknowledges that Creative Commons is not a + party to this document and has no duty or obligation with respect to + this CC0 or use of the Work. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..fc07760 --- /dev/null +++ b/README.md @@ -0,0 +1,9 @@ +# Cardinal Cooperative Capitalism: Market Economy + +## Setup + +For setup instructions, please see the [Fabric Documentation page](https://docs.fabricmc.net/develop/getting-started/creating-a-project#setting-up) related to the IDE that you are using. + +## License + +This template is available under the CC0 license. Feel free to learn from it and incorporate it in your own projects. diff --git a/TODO b/TODO new file mode 100644 index 0000000..2cca3aa --- /dev/null +++ b/TODO @@ -0,0 +1 @@ +礼物,信,神器,点歌 \ No newline at end of file diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..d10e8e5 --- /dev/null +++ b/build.gradle @@ -0,0 +1,88 @@ +plugins { + id 'net.fabricmc.fabric-loom-remap' version "${loom_version}" + id 'maven-publish' +} + +version = project.mod_version +group = project.maven_group + +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. +} + +loom { + mods { + "cardinal-cooperative-capitalism-market-economy" { + sourceSet sourceSets.main + } + } +} + +dependencies { + minecraft "com.mojang:minecraft:${project.minecraft_version}" + mappings "net.fabricmc:yarn:${project.yarn_mappings}:v2" + modImplementation "net.fabricmc:fabric-loader:${project.loader_version}" + + modImplementation "net.fabricmc.fabric-api:fabric-api:${project.fabric_api_version}" + + implementation("org.xerial:sqlite-jdbc:3.45.1.0") + implementation("com.mysql:mysql-connector-j:8.3.0") + implementation("com.zaxxer:HikariCP:5.1.0") + + include("org.xerial:sqlite-jdbc:3.45.1.0") + include("com.mysql:mysql-connector-j:8.3.0") + include("com.zaxxer:HikariCP:5.1.0") +} + +processResources { + def version = project.version + inputs.property "version", version + + filesMatching("fabric.mod.json") { + expand "version": version + } +} + +tasks.withType(JavaCompile).configureEach { + it.options.release = 21 +} + +java { + // 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() + + sourceCompatibility = JavaVersion.VERSION_21 + targetCompatibility = JavaVersion.VERSION_21 +} + +jar { + def projectName = project.name + inputs.property "projectName", projectName + + from("LICENSE") { + rename { "${it}_$projectName"} + } +} + +// configure the maven publication +publishing { + publications { + create("mavenJava", MavenPublication) { + from components.java + } + } + + // See https://docs.gradle.org/current/userguide/publishing_maven.html for information on how to set up publishing. + repositories { + // Add repositories to publish to here. + // Notice: This block does NOT have the same function as the block in the top level. + // The repositories here will be used for publishing your artifact, not for + // retrieving dependencies. + } +} diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..c5a8f52 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,20 @@ +# Done to increase the memory available to gradle. +org.gradle.jvmargs=-Xmx1G +org.gradle.parallel=true + +# IntelliJ IDEA is not yet fully compatible with configuration cache, see: https://github.com/FabricMC/fabric-loom/issues/1349 +org.gradle.configuration-cache=false + +# Fabric Properties +# check these on https://fabricmc.net/develop +minecraft_version=1.21.10 +yarn_mappings=1.21.10+build.3 +loader_version=0.19.2 +loom_version=1.16-SNAPSHOT + +# Mod Properties +mod_version=1.0.0 +maven_group=com.gvsds.tcme + +# Dependencies +fabric_api_version=0.138.4+1.21.10 \ No newline at end of file diff --git a/gradle/init.gradle b/gradle/init.gradle new file mode 100644 index 0000000..1aa0fc7 --- /dev/null +++ b/gradle/init.gradle @@ -0,0 +1,40 @@ +allprojects { + buildscript { + repositories { + maven { + name 'Aliyun' + url 'https://maven.aliyun.com/repository/central' + } + maven { + name 'AliyunPublic' + url 'https://maven.aliyun.com/repository/public' + } + maven { + name 'Tencent' + url 'https://mirrors.cloud.tencent.com/nexus/repository/maven-public/' + } + mavenCentral() + gradlePluginPortal() + } + } + + repositories { + maven { + name 'Aliyun' + url 'https://maven.aliyun.com/repository/central' + } + maven { + name 'AliyunPublic' + url 'https://maven.aliyun.com/repository/public' + } + maven { + name 'Tencent' + url 'https://mirrors.cloud.tencent.com/nexus/repository/maven-public/' + } + maven { + name 'Modrinth' + url 'https://api.modrinth.com/maven' + } + mavenCentral() + } +} diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..d997cfc Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..8f23c7a --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=file:///D:/%E8%BF%85%E9%9B%B7%E4%B8%8B%E8%BD%BD/gradle-9.4.1-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100644 index 0000000..739907d --- /dev/null +++ b/gradlew @@ -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/2d6327017519d23b96af35865dc997fcb544fb40/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" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..c4bdd3a --- /dev/null +++ b/gradlew.bat @@ -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 diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..10b349d --- /dev/null +++ b/settings.gradle @@ -0,0 +1,58 @@ +pluginManagement { + repositories { + // 阿里云镜像 + maven { + name = 'Aliyun' + url = 'https://maven.aliyun.com/repository/central' + } + maven { + name = 'AliyunPublic' + url = 'https://maven.aliyun.com/repository/public' + } + // 腾讯云镜像 + maven { + name = 'Tencent' + url = 'https://mirrors.cloud.tencent.com/nexus/repository/maven-public/' + } + // Fabric 官方源 + maven { + name = 'Fabric' + url = 'https://maven.fabricmc.net/' + } + mavenCentral() + gradlePluginPortal() + } +} + +dependencyResolutionManagement { + repositories { + // 阿里云镜像 - 用于通用依赖 + maven { + name = 'Aliyun' + url = 'https://maven.aliyun.com/repository/central' + } + maven { + name = 'AliyunPublic' + url = 'https://maven.aliyun.com/repository/public' + } + // 腾讯云镜像 + maven { + name = 'Tencent' + url = 'https://mirrors.cloud.tencent.com/nexus/repository/maven-public/' + } + // Modrinth 镜像 - 用于 Minecraft 模组相关依赖 + maven { + name = 'Modrinth' + url = 'https://api.modrinth.com/maven' + } + // Fabric 官方源 - 用于 Fabric 相关依赖 + maven { + name = 'Fabric' + url = 'https://maven.fabricmc.net/' + } + mavenCentral() + } +} + +// Should match your modid +rootProject.name = 'cardinal-cooperative-capitalism-market-economy' diff --git a/src/main/java/com/gvsds/tcme/CardinalCooperativeCapitalismMarketEconomy.java b/src/main/java/com/gvsds/tcme/CardinalCooperativeCapitalismMarketEconomy.java new file mode 100644 index 0000000..7b9b60d --- /dev/null +++ b/src/main/java/com/gvsds/tcme/CardinalCooperativeCapitalismMarketEconomy.java @@ -0,0 +1,124 @@ +package com.gvsds.tcme; + +import com.gvsds.tcme.command.CMECommand; +import com.gvsds.tcme.config.Config; +import com.gvsds.tcme.config.ConfigManager; +import com.gvsds.tcme.database.DatabaseManager; +import com.gvsds.tcme.gui.InputScreenHandler; +import com.gvsds.tcme.manager.BalanceManager; +import com.gvsds.tcme.manager.CurrencyManager; +import com.gvsds.tcme.manager.ExchangeRateManager; +import com.gvsds.tcme.manager.PaymentRequestManager; +import com.gvsds.tcme.manager.PlayerSettingsManager; +import com.gvsds.tcme.manager.TransactionManager; +import com.gvsds.tcme.i18n.LanguageManager; +import net.fabricmc.api.ModInitializer; +import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents; +import net.fabricmc.fabric.api.message.v1.ServerMessageEvents; +import net.minecraft.text.Text; +import net.minecraft.util.Formatting; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class CardinalCooperativeCapitalismMarketEconomy implements ModInitializer { + public static final String MOD_ID = "cardinal-cooperative-capitalism-market-economy"; + + public static final Logger LOGGER = LoggerFactory.getLogger(MOD_ID); + + private static ConfigManager configManager; + private static DatabaseManager databaseManager; + private static CurrencyManager currencyManager; + private static BalanceManager balanceManager; + private static TransactionManager transactionManager; + private static PaymentRequestManager paymentRequestManager; + private static PlayerSettingsManager playerSettingsManager; + private static LanguageManager languageManager; + private static ExchangeRateManager exchangeRateManager; + + @Override + public void onInitialize() { + LOGGER.info("Initializing Cardinal Cooperative Capitalism: Market Economy"); + + configManager = new ConfigManager(); + LOGGER.info("Configuration loaded successfully"); + + CMECommand command = new CMECommand(); + command.register(); + LOGGER.info("CME command tree registered via CommandRegistrationCallback"); + + ServerLifecycleEvents.SERVER_STARTING.register(server -> { + LOGGER.info("Server starting, initializing database and managers..."); + Config.Database dbConfig = configManager.getConfig().getDatabase(); + databaseManager = new DatabaseManager(dbConfig); + databaseManager.connect(); + + if (databaseManager.isConnected()) { + boolean isMySQL = "mysql".equalsIgnoreCase(dbConfig.getType()); + Config.Tables tables = dbConfig.getTables(); + + currencyManager = new CurrencyManager(databaseManager, tables); + balanceManager = new BalanceManager(databaseManager, tables, isMySQL); + transactionManager = new TransactionManager(databaseManager, tables, balanceManager, isMySQL); + paymentRequestManager = new PaymentRequestManager(databaseManager, tables, balanceManager, transactionManager, isMySQL); + playerSettingsManager = new PlayerSettingsManager(databaseManager, tables, isMySQL); + exchangeRateManager = new ExchangeRateManager(databaseManager, tables, isMySQL); + languageManager = new LanguageManager(databaseManager, isMySQL); + + CMECommand.setManagers(currencyManager, balanceManager, transactionManager, paymentRequestManager, playerSettingsManager, languageManager, exchangeRateManager); + LOGGER.info("Managers initialized and set to CMECommand"); + } + }); + + ServerMessageEvents.CHAT_MESSAGE.register((message, sender, params) -> { + String text = message.getContent().getString(); + if (InputScreenHandler.hasSession(sender.getUuid())) { + InputScreenHandler.submitInput(sender, text); + } + }); + + ServerLifecycleEvents.SERVER_STOPPING.register(server -> { + LOGGER.info("Server stopping, closing database connection..."); + if (databaseManager != null) { + databaseManager.disconnect(); + } + }); + + LOGGER.info("Initialization complete!"); + } + + public static ConfigManager getConfigManager() { + return configManager; + } + + public static DatabaseManager getDatabaseManager() { + return databaseManager; + } + + public static CurrencyManager getCurrencyManager() { + return currencyManager; + } + + public static BalanceManager getBalanceManager() { + return balanceManager; + } + + public static TransactionManager getTransactionManager() { + return transactionManager; + } + + public static PaymentRequestManager getPaymentRequestManager() { + return paymentRequestManager; + } + + public static PlayerSettingsManager getPlayerSettingsManager() { + return playerSettingsManager; + } + + public static LanguageManager getLanguageManager() { + return languageManager; + } + + public static ExchangeRateManager getExchangeRateManager() { + return exchangeRateManager; + } +} diff --git a/src/main/java/com/gvsds/tcme/command/CMECommand.java b/src/main/java/com/gvsds/tcme/command/CMECommand.java new file mode 100644 index 0000000..5a22403 --- /dev/null +++ b/src/main/java/com/gvsds/tcme/command/CMECommand.java @@ -0,0 +1,2620 @@ +package com.gvsds.tcme.command; + +import com.mojang.brigadier.CommandDispatcher; +import com.mojang.brigadier.arguments.BoolArgumentType; +import com.mojang.brigadier.arguments.DoubleArgumentType; +import com.mojang.brigadier.arguments.IntegerArgumentType; +import com.mojang.brigadier.arguments.StringArgumentType; +import com.mojang.brigadier.context.CommandContext; +import net.fabricmc.fabric.api.command.v2.CommandRegistrationCallback; +import net.fabricmc.fabric.api.event.lifecycle.v1.ServerTickEvents; +import net.minecraft.command.argument.EntityArgumentType; +import net.minecraft.entity.EntityType; +import net.minecraft.entity.LightningEntity; +import net.minecraft.entity.SpawnReason; +import net.minecraft.entity.boss.BossBar; +import net.minecraft.entity.boss.ServerBossBar; +import net.minecraft.particle.ParticleTypes; +import net.minecraft.server.command.ServerCommandSource; +import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.server.world.ServerWorld; +import net.minecraft.sound.SoundCategory; +import net.minecraft.sound.SoundEvents; +import net.minecraft.text.Text; +import net.minecraft.util.Formatting; +import net.minecraft.util.hit.BlockHitResult; +import net.minecraft.util.hit.HitResult; +import net.minecraft.block.BlockState; +import net.minecraft.block.Blocks; +import net.minecraft.block.Block; +import net.minecraft.block.entity.BlockEntity; +import net.minecraft.inventory.Inventory; +import net.minecraft.nbt.NbtCompound; +import net.minecraft.util.math.BlockPos; +import net.minecraft.world.Heightmap; +import net.minecraft.world.RaycastContext; +import net.minecraft.world.World; + +import com.gvsds.tcme.gui.ServerGuiAPI; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.UUID; + +import static net.minecraft.server.command.CommandManager.argument; +import static net.minecraft.server.command.CommandManager.literal; + +public class CMECommand { + + private static com.gvsds.tcme.manager.CurrencyManager currencyManager; + private static com.gvsds.tcme.manager.BalanceManager balanceManager; + private static com.gvsds.tcme.manager.TransactionManager transactionManager; + private static com.gvsds.tcme.manager.PaymentRequestManager paymentRequestManager; + private static com.gvsds.tcme.manager.PlayerSettingsManager playerSettingsManager; + private static com.gvsds.tcme.i18n.LanguageManager languageManager; + private static com.gvsds.tcme.manager.ExchangeRateManager exchangeRateManager; + + private static final List BoomSessions = new ArrayList<>(); + private static final List MeteoSessions = new ArrayList<>(); + private static final List LineSessions = new ArrayList<>(); + private static final java.util.concurrent.atomic.AtomicInteger NextTestId = new java.util.concurrent.atomic.AtomicInteger(1); + + private interface TestSession { + int getId(); + void cancel(); + String getType(); + String getDescription(); + } + + private static final class GlobalBlockRegistry { + private static final Map> OriginalStates = new HashMap<>(); + private static final Map> OriginalNbt = new HashMap<>(); + private static final Map> RefCounts = new HashMap<>(); + + private static Map getStates(ServerWorld world) { + return OriginalStates.computeIfAbsent(world, k -> new HashMap<>()); + } + private static Map getNbt(ServerWorld world) { + return OriginalNbt.computeIfAbsent(world, k -> new HashMap<>()); + } + private static Map getCounts(ServerWorld world) { + return RefCounts.computeIfAbsent(world, k -> new HashMap<>()); + } + + static boolean acquire(ServerWorld world, BlockPos pos, BlockState currentState, NbtCompound currentNbt) { + Map states = getStates(world); + Map nbt = getNbt(world); + Map counts = getCounts(world); + + int count = counts.getOrDefault(pos, 0); + if (count == 0) { + states.put(pos, currentState); + if (currentNbt != null) nbt.put(pos, currentNbt); + } + counts.put(pos, count + 1); + return count == 0; + } + + static boolean release(ServerWorld world, BlockPos pos) { + Map counts = getCounts(world); + Integer count = counts.get(pos); + if (count == null) return false; + if (count <= 1) { + counts.remove(pos); + return true; + } else { + counts.put(pos, count - 1); + return false; + } + } + + static BlockState getOriginalState(ServerWorld world, BlockPos pos) { + Map states = OriginalStates.get(world); + return states != null ? states.get(pos) : null; + } + + static NbtCompound getOriginalNbt(ServerWorld world, BlockPos pos) { + Map nbt = OriginalNbt.get(world); + return nbt != null ? nbt.get(pos) : null; + } + + static void clearEntry(ServerWorld world, BlockPos pos) { + Map states = OriginalStates.get(world); + Map nbt = OriginalNbt.get(world); + if (states != null) states.remove(pos); + if (nbt != null) nbt.remove(pos); + } + } + + public CMECommand() { + } + + public static void setManagers(com.gvsds.tcme.manager.CurrencyManager currencyManager, + com.gvsds.tcme.manager.BalanceManager balanceManager, + com.gvsds.tcme.manager.TransactionManager transactionManager, + com.gvsds.tcme.manager.PaymentRequestManager paymentRequestManager, + com.gvsds.tcme.manager.PlayerSettingsManager playerSettingsManager, + com.gvsds.tcme.i18n.LanguageManager languageManager, + com.gvsds.tcme.manager.ExchangeRateManager exchangeRateManager) { + CMECommand.currencyManager = currencyManager; + CMECommand.balanceManager = balanceManager; + CMECommand.transactionManager = transactionManager; + CMECommand.paymentRequestManager = paymentRequestManager; + CMECommand.playerSettingsManager = playerSettingsManager; + CMECommand.languageManager = languageManager; + CMECommand.exchangeRateManager = exchangeRateManager; + } + + public void register() { + CommandRegistrationCallback.EVENT.register((dispatcher, registryAccess, environment) -> { + buildCommandTree(dispatcher); + }); + registerTickHandler(); + } + + public void registerDirect(CommandDispatcher dispatcher) { + buildCommandTree(dispatcher); + registerTickHandler(); + } + + private static boolean tickHandlerRegistered = false; + + private void registerTickHandler() { + if (tickHandlerRegistered) return; + tickHandlerRegistered = true; + ServerTickEvents.END_SERVER_TICK.register(server -> { + if (!BoomSessions.isEmpty()) { + Iterator iterator = BoomSessions.iterator(); + while (iterator.hasNext()) { + BoomSession session = iterator.next(); + session.tick(); + if (session.isFinished()) { + iterator.remove(); + } + } + } + if (!MeteoSessions.isEmpty()) { + Iterator iterator = MeteoSessions.iterator(); + while (iterator.hasNext()) { + MeteoSession session = iterator.next(); + session.tick(); + if (session.isFinished()) { + iterator.remove(); + } + } + } + if (!LineSessions.isEmpty()) { + Iterator iterator = LineSessions.iterator(); + while (iterator.hasNext()) { + LineSession session = iterator.next(); + session.tick(); + if (session.isFinished()) { + iterator.remove(); + } + } + } + }); + } + + private void buildCommandTree(CommandDispatcher dispatcher) { + var root = literal("cme").requires(source -> source.hasPermissionLevel(0)); + + var admin = literal("admin").requires(source -> source.hasPermissionLevel(4)); + + var coin = literal("coin"); + coin.then(literal("add") + .then(argument("name", StringArgumentType.string()) + .then(argument("symbol", StringArgumentType.string()) + .then(argument("tradable", BoolArgumentType.bool()) + .executes(ctx -> addCoin(ctx, StringArgumentType.getString(ctx, "name"), + StringArgumentType.getString(ctx, "symbol"), + BoolArgumentType.getBool(ctx, "tradable"))))))); + coin.then(literal("delete") + .then(argument("name", StringArgumentType.string()) + .executes(ctx -> deleteCoin(ctx, StringArgumentType.getString(ctx, "name"))))); + coin.then(literal("tradable") + .then(argument("name", StringArgumentType.string()) + .then(argument("tradable", BoolArgumentType.bool()) + .executes(ctx -> setTradable(ctx, StringArgumentType.getString(ctx, "name"), + BoolArgumentType.getBool(ctx, "tradable")))))); + coin.then(literal("list") + .executes(this::listCoins)); + coin.then(literal("rename") + .then(argument("oldName", StringArgumentType.string()) + .then(argument("newName", StringArgumentType.string()) + .executes(ctx -> renameCoin(ctx, StringArgumentType.getString(ctx, "oldName"), + StringArgumentType.getString(ctx, "newName")))))); + coin.then(literal("edit") + .then(argument("name", StringArgumentType.string()) + .then(argument("symbol", StringArgumentType.string()) + .then(argument("tradable", BoolArgumentType.bool()) + .executes(ctx -> editCoin(ctx, StringArgumentType.getString(ctx, "name"), + StringArgumentType.getString(ctx, "symbol"), + BoolArgumentType.getBool(ctx, "tradable"))))))); + coin.then(literal("rate") + .then(argument("currencyName", StringArgumentType.string()) + .then(argument("rate", DoubleArgumentType.doubleArg(0)) + .executes(ctx -> setExchangeRate(ctx, StringArgumentType.getString(ctx, "currencyName"), + DoubleArgumentType.getDouble(ctx, "rate"))))) + .executes(this::listExchangeRates)); + + admin.then(coin); + + var balance = literal("balance"); + balance.then(literal("check") + .then(argument("player", EntityArgumentType.player()) + .then(argument("currencyName", StringArgumentType.string()) + .executes(ctx -> checkBalance(ctx, EntityArgumentType.getPlayer(ctx, "player"), + StringArgumentType.getString(ctx, "currencyName")))))); + balance.then(literal("add") + .then(argument("player", EntityArgumentType.player()) + .then(argument("currencyName", StringArgumentType.string()) + .then(argument("amount", DoubleArgumentType.doubleArg(0)) + .executes(ctx -> adminModifyBalance(ctx, EntityArgumentType.getPlayer(ctx, "player"), + StringArgumentType.getString(ctx, "currencyName"), + DoubleArgumentType.getDouble(ctx, "amount"), + true)))))); + balance.then(literal("remove") + .then(argument("player", EntityArgumentType.player()) + .then(argument("currencyName", StringArgumentType.string()) + .then(argument("amount", DoubleArgumentType.doubleArg(0)) + .executes(ctx -> adminModifyBalance(ctx, EntityArgumentType.getPlayer(ctx, "player"), + StringArgumentType.getString(ctx, "currencyName"), + DoubleArgumentType.getDouble(ctx, "amount"), + false)))))); + + admin.then(balance); + + root.then(admin); + + root.then(literal("wallet") + .executes(this::showWallet)); + + root.then(literal("lang") + .executes(this::showLanguageList) + .then(argument("language", StringArgumentType.string()) + .executes(ctx -> setLanguage(ctx, StringArgumentType.getString(ctx, "language"))))); + + root.then(literal("convert") + .then(argument("amount", DoubleArgumentType.doubleArg(0)) + .then(argument("fromCurrency", StringArgumentType.string()) + .then(argument("toCurrency", StringArgumentType.string()) + .executes(ctx -> previewConvert(ctx, DoubleArgumentType.getDouble(ctx, "amount"), + StringArgumentType.getString(ctx, "fromCurrency"), + StringArgumentType.getString(ctx, "toCurrency"))) + .then(literal("confirm") + .executes(ctx -> executeConvert(ctx, DoubleArgumentType.getDouble(ctx, "amount"), + StringArgumentType.getString(ctx, "fromCurrency"), + StringArgumentType.getString(ctx, "toCurrency")))))))); + + root.then(literal("pay") + .then(argument("player", EntityArgumentType.player()) + .then(argument("amount", DoubleArgumentType.doubleArg(0)) + .executes(ctx -> pay(ctx, EntityArgumentType.getPlayer(ctx, "player"), + DoubleArgumentType.getDouble(ctx, "amount"), + null)) + .then(argument("currency", StringArgumentType.string()) + .executes(ctx -> pay(ctx, EntityArgumentType.getPlayer(ctx, "player"), + DoubleArgumentType.getDouble(ctx, "amount"), + StringArgumentType.getString(ctx, "currency"))))))); + + root.then(literal("apay") + .then(argument("player", EntityArgumentType.player()) + .then(argument("amount", DoubleArgumentType.doubleArg(0)) + .executes(ctx -> apay(ctx, EntityArgumentType.getPlayer(ctx, "player"), + DoubleArgumentType.getDouble(ctx, "amount"), + null)) + .then(argument("currency", StringArgumentType.string()) + .executes(ctx -> apay(ctx, EntityArgumentType.getPlayer(ctx, "player"), + DoubleArgumentType.getDouble(ctx, "amount"), + StringArgumentType.getString(ctx, "currency"))))))); + + root.then(literal("requests") + .executes(this::listRequests) + .then(literal("accept") + .then(argument("requestId", IntegerArgumentType.integer()) + .executes(ctx -> acceptRequest(ctx, IntegerArgumentType.getInteger(ctx, "requestId"))))) + .then(literal("reject") + .then(argument("requestId", IntegerArgumentType.integer()) + .executes(ctx -> rejectRequest(ctx, IntegerArgumentType.getInteger(ctx, "requestId"))))) + .then(literal("cancel") + .then(argument("requestId", IntegerArgumentType.integer()) + .executes(ctx -> cancelRequest(ctx, IntegerArgumentType.getInteger(ctx, "requestId")))))); + + var uidemo = literal("uidemo"); + uidemo.then(literal("alert") + .executes(this::demoAlert)); + uidemo.then(literal("confirm") + .executes(this::demoConfirm)); + uidemo.then(literal("input") + .executes(this::demoInput)); + uidemo.then(literal("smallchest") + .executes(this::demoSmallChest)); + uidemo.then(literal("mediumchest") + .executes(this::demoMediumChest)); + uidemo.then(literal("largechest") + .executes(this::demoLargeChest)); + uidemo.then(literal("anvil") + .executes(this::demoAnvil)); + + root.then(uidemo); + + var test = literal("test").requires(source -> source.hasPermissionLevel(2)); + test.then(literal("boom") + .executes(ctx -> boomRaycast(ctx, 10, 5)) + .then(argument("x", IntegerArgumentType.integer()) + .suggests((ctx, builder) -> { + BlockPos pos = getTargetedBlockPos(ctx.getSource()); + if (pos != null) builder.suggest(pos.getX()); + return builder.buildFuture(); + }) + .then(argument("y", IntegerArgumentType.integer()) + .suggests((ctx, builder) -> { + BlockPos pos = getTargetedBlockPos(ctx.getSource()); + if (pos != null) builder.suggest(pos.getY()); + return builder.buildFuture(); + }) + .then(argument("z", IntegerArgumentType.integer()) + .suggests((ctx, builder) -> { + BlockPos pos = getTargetedBlockPos(ctx.getSource()); + if (pos != null) builder.suggest(pos.getZ()); + return builder.buildFuture(); + }) + .executes(ctx -> boom(ctx, IntegerArgumentType.getInteger(ctx, "x"), IntegerArgumentType.getInteger(ctx, "y"), IntegerArgumentType.getInteger(ctx, "z"), 10, 5)) + .then(argument("range", IntegerArgumentType.integer(1)) + .executes(ctx -> boom(ctx, IntegerArgumentType.getInteger(ctx, "x"), IntegerArgumentType.getInteger(ctx, "y"), IntegerArgumentType.getInteger(ctx, "z"), IntegerArgumentType.getInteger(ctx, "range"), 5)) + .then(argument("depth", IntegerArgumentType.integer(-1)) + .executes(ctx -> boom(ctx, IntegerArgumentType.getInteger(ctx, "x"), IntegerArgumentType.getInteger(ctx, "y"), IntegerArgumentType.getInteger(ctx, "z"), IntegerArgumentType.getInteger(ctx, "range"), IntegerArgumentType.getInteger(ctx, "depth"))))))))); + test.then(literal("meteo") + .executes(ctx -> meteoRaycast(ctx)) + .then(argument("x", IntegerArgumentType.integer()) + .suggests((ctx, builder) -> { + BlockPos pos = getTargetedBlockPos(ctx.getSource()); + if (pos != null) builder.suggest(pos.getX()); + return builder.buildFuture(); + }) + .then(argument("y", IntegerArgumentType.integer()) + .suggests((ctx, builder) -> { + BlockPos pos = getTargetedBlockPos(ctx.getSource()); + if (pos != null) builder.suggest(pos.getY()); + return builder.buildFuture(); + }) + .then(argument("z", IntegerArgumentType.integer()) + .suggests((ctx, builder) -> { + BlockPos pos = getTargetedBlockPos(ctx.getSource()); + if (pos != null) builder.suggest(pos.getZ()); + return builder.buildFuture(); + }) + .executes(ctx -> meteo(ctx, IntegerArgumentType.getInteger(ctx, "x"), IntegerArgumentType.getInteger(ctx, "y"), IntegerArgumentType.getInteger(ctx, "z"))))))); + test.then(literal("line") + .executes(ctx -> lineRaycast(ctx, 10)) + .then(argument("x", IntegerArgumentType.integer()) + .suggests((ctx, builder) -> { + BlockPos pos = getTargetedBlockPos(ctx.getSource()); + if (pos != null) builder.suggest(pos.getX()); + return builder.buildFuture(); + }) + .then(argument("z", IntegerArgumentType.integer()) + .suggests((ctx, builder) -> { + BlockPos pos = getTargetedBlockPos(ctx.getSource()); + if (pos != null) builder.suggest(pos.getZ()); + return builder.buildFuture(); + }) + .executes(ctx -> line(ctx, IntegerArgumentType.getInteger(ctx, "x"), IntegerArgumentType.getInteger(ctx, "z"), 10)) + .then(argument("range", IntegerArgumentType.integer(1)) + .executes(ctx -> line(ctx, IntegerArgumentType.getInteger(ctx, "x"), IntegerArgumentType.getInteger(ctx, "z"), IntegerArgumentType.getInteger(ctx, "range"))))))); + test.then(literal("sq") + .executes(ctx -> sqDemoRaycast(ctx)) + .then(argument("x", IntegerArgumentType.integer()) + .suggests((ctx, builder) -> { + BlockPos pos = getTargetedBlockPos(ctx.getSource()); + if (pos != null) builder.suggest(pos.getX()); + return builder.buildFuture(); + }) + .then(argument("y", IntegerArgumentType.integer()) + .suggests((ctx, builder) -> { + BlockPos pos = getTargetedBlockPos(ctx.getSource()); + if (pos != null) builder.suggest(pos.getY()); + return builder.buildFuture(); + }) + .then(argument("z", IntegerArgumentType.integer()) + .suggests((ctx, builder) -> { + BlockPos pos = getTargetedBlockPos(ctx.getSource()); + if (pos != null) builder.suggest(pos.getZ()); + return builder.buildFuture(); + }) + .executes(ctx -> sqDemo(ctx, IntegerArgumentType.getInteger(ctx, "x"), IntegerArgumentType.getInteger(ctx, "y"), IntegerArgumentType.getInteger(ctx, "z"), 10)) + .then(argument("range", IntegerArgumentType.integer(1)) + .executes(ctx -> sqDemo(ctx, IntegerArgumentType.getInteger(ctx, "x"), IntegerArgumentType.getInteger(ctx, "y"), IntegerArgumentType.getInteger(ctx, "z"), IntegerArgumentType.getInteger(ctx, "range")))))))); + test.then(literal("list") + .executes(this::listTestSessions)); + test.then(literal("cancel") + .then(argument("id", IntegerArgumentType.integer(1)) + .executes(ctx -> cancelTestSession(ctx, IntegerArgumentType.getInteger(ctx, "id"))))); + root.then(test); + + dispatcher.register(root); + } + + private void sendMsg(ServerPlayerEntity player, String key, Object... args) { + player.sendMessage(Text.literal(String.format(languageManager.getMessageRaw(player.getUuid(), key), args)).formatted(Formatting.WHITE)); + } + + private void sendFeedback(ServerCommandSource source, String key, Object... args) { + ServerPlayerEntity player = source.getPlayer(); + if (player != null) { + source.sendFeedback(() -> Text.literal(String.format(languageManager.getMessageRaw(player.getUuid(), key), args)).formatted(Formatting.WHITE), false); + } else { + source.sendFeedback(() -> Text.literal(String.format(key, args)).formatted(Formatting.WHITE), false); + } + } + + private int addCoin(CommandContext ctx, String name, String symbol, boolean tradable) { + ServerPlayerEntity player = ctx.getSource().getPlayer(); + Optional result = currencyManager.createCurrency(name, symbol, tradable); + if (result.isPresent()) { + sendFeedback(ctx.getSource(), "cme.currency.created", name, symbol, tradable); + return 1; + } + sendFeedback(ctx.getSource(), "cme.currency.create_failed", name); + return 0; + } + + private int deleteCoin(CommandContext ctx, String name) { + if (currencyManager.deleteCurrency(name)) { + sendFeedback(ctx.getSource(), "cme.currency.deleted", name); + return 1; + } + sendFeedback(ctx.getSource(), "cme.currency.delete_failed", name); + return 0; + } + + private int renameCoin(CommandContext ctx, String oldName, String newName) { + if (currencyManager.renameCurrency(oldName, newName)) { + sendFeedback(ctx.getSource(), "cme.currency.renamed", oldName, newName); + return 1; + } + sendFeedback(ctx.getSource(), "cme.currency.rename_failed", oldName); + return 0; + } + + private int editCoin(CommandContext ctx, String name, String symbol, boolean tradable) { + if (currencyManager.updateCurrency(name, symbol, tradable)) { + sendFeedback(ctx.getSource(), "cme.currency.updated", name, symbol, tradable); + return 1; + } + sendFeedback(ctx.getSource(), "cme.currency.update_failed", name); + return 0; + } + + private int setTradable(CommandContext ctx, String name, boolean tradable) { + Optional currency = currencyManager.getCurrencyByName(name); + if (currency.isEmpty()) { + sendFeedback(ctx.getSource(), "cme.currency.not_found", name); + return 0; + } + if (currencyManager.setTradable(currency.get().getId(), tradable)) { + sendFeedback(ctx.getSource(), "cme.currency.tradable", name, tradable); + return 1; + } + sendFeedback(ctx.getSource(), "cme.currency.tradable_failed", name); + return 0; + } + + private int listCoins(CommandContext ctx) { + List currencies = currencyManager.getAllCurrencies(); + if (currencies.isEmpty()) { + sendFeedback(ctx.getSource(), "cme.currency.no_currencies"); + return 0; + } + sendFeedback(ctx.getSource(), "cme.currency.list"); + for (com.gvsds.tcme.model.Currency c : currencies) { + double rate = exchangeRateManager.getRate(c.getId()); + sendFeedback(ctx.getSource(), "cme.currency.list_item", c.getName(), c.getSymbol(), c.isTradable(), rate); + } + return 1; + } + + private int checkBalance(CommandContext ctx, ServerPlayerEntity player, String currencyName) { + Optional currency = currencyManager.getCurrencyByName(currencyName); + if (currency.isEmpty()) { + sendFeedback(ctx.getSource(), "cme.currency.not_found", currencyName); + return 0; + } + double balance = balanceManager.getBalance(player.getUuid(), currency.get().getId()); + sendFeedback(ctx.getSource(), "cme.balance.check", player.getName().getString(), balance, currency.get().getSymbol()); + return 1; + } + + private int adminModifyBalance(CommandContext ctx, ServerPlayerEntity player, String currencyName, double amount, boolean add) { + Optional currency = currencyManager.getCurrencyByName(currencyName); + if (currency.isEmpty()) { + sendFeedback(ctx.getSource(), "cme.currency.not_found", currencyName); + return 0; + } + + boolean success; + if (add) { + success = balanceManager.addBalance(player.getUuid(), currency.get().getId(), amount); + } else { + success = balanceManager.subtractBalance(player.getUuid(), currency.get().getId(), amount); + } + + if (success) { + String action = add ? "Added" : "Removed"; + sendFeedback(ctx.getSource(), "cme.balance." + (add ? "added" : "removed"), amount, currency.get().getSymbol(), player.getName().getString()); + return 1; + } + sendFeedback(ctx.getSource(), "cme.balance.failed", add ? "add" : "remove"); + return 0; + } + + private int pay(CommandContext ctx, ServerPlayerEntity target, double amount, String currencyName) { + ServerPlayerEntity source = ctx.getSource().getPlayer(); + if (source == null) return 0; + + int currencyId; + com.gvsds.tcme.model.Currency currency; + + if (currencyName == null) { + List currencies = currencyManager.getAllCurrencies(); + if (currencies.isEmpty()) { + source.sendMessage(Text.literal(languageManager.getMessageRaw(source.getUuid(), "cme.currency.no_available")).formatted(Formatting.RED)); + return 0; + } + currency = currencies.get(0); + currencyId = currency.getId(); + } else { + Optional opt = currencyManager.getCurrencyByName(currencyName); + if (opt.isEmpty()) { + source.sendMessage(Text.literal(String.format(languageManager.getMessageRaw(source.getUuid(), "cme.currency.not_found"), currencyName)).formatted(Formatting.RED)); + return 0; + } + currency = opt.get(); + currencyId = currency.getId(); + } + + if (!currency.isTradable()) { + source.sendMessage(Text.literal(String.format(languageManager.getMessageRaw(source.getUuid(), "cme.currency.not_tradable"), currency.getName())).formatted(Formatting.RED)); + return 0; + } + + if (source.getUuid().equals(target.getUuid())) { + source.sendMessage(Text.literal(languageManager.getMessageRaw(source.getUuid(), "cme.pay.self")).formatted(Formatting.RED)); + return 0; + } + + if (transactionManager.transfer(source.getUuid(), target.getUuid(), currencyId, amount, "pay", "Player transfer")) { + source.sendMessage(Text.literal(String.format(languageManager.getMessageRaw(source.getUuid(), "cme.pay.success"), amount, currency.getSymbol(), target.getName().getString())).formatted(Formatting.GREEN)); + target.sendMessage(Text.literal(String.format(languageManager.getMessageRaw(target.getUuid(), "cme.pay.received"), source.getName().getString(), amount, currency.getSymbol())).formatted(Formatting.GREEN)); + return 1; + } + source.sendMessage(Text.literal(languageManager.getMessageRaw(source.getUuid(), "cme.pay.failed")).formatted(Formatting.RED)); + return 0; + } + + private int apay(CommandContext ctx, ServerPlayerEntity target, double amount, String currencyName) { + ServerPlayerEntity source = ctx.getSource().getPlayer(); + if (source == null) return 0; + + int currencyId; + com.gvsds.tcme.model.Currency currency; + + if (currencyName == null) { + List currencies = currencyManager.getAllCurrencies(); + if (currencies.isEmpty()) { + source.sendMessage(Text.literal(languageManager.getMessageRaw(source.getUuid(), "cme.currency.no_available")).formatted(Formatting.RED)); + return 0; + } + currency = currencies.get(0); + currencyId = currency.getId(); + } else { + Optional opt = currencyManager.getCurrencyByName(currencyName); + if (opt.isEmpty()) { + source.sendMessage(Text.literal(String.format(languageManager.getMessageRaw(source.getUuid(), "cme.currency.not_found"), currencyName)).formatted(Formatting.RED)); + return 0; + } + currency = opt.get(); + currencyId = currency.getId(); + } + + if (source.getUuid().equals(target.getUuid())) { + source.sendMessage(Text.literal(languageManager.getMessageRaw(source.getUuid(), "cme.apay.self")).formatted(Formatting.RED)); + return 0; + } + + Optional request = paymentRequestManager.createRequest(source.getUuid(), target.getUuid(), currencyId, amount); + if (request.isPresent()) { + source.sendMessage(Text.literal(String.format(languageManager.getMessageRaw(source.getUuid(), "cme.apay.sent"), target.getName().getString(), amount, currency.getSymbol(), request.get().getId())).formatted(Formatting.GREEN)); + target.sendMessage(Text.literal(String.format(languageManager.getMessageRaw(target.getUuid(), "cme.apay.received"), source.getName().getString(), amount, currency.getSymbol(), request.get().getId())).formatted(Formatting.YELLOW)); + + target.sendMessage(Text.literal(String.format("§e§l%s §r§7向你发起了付款请求: §e%.2f %s", source.getName().getString(), amount, currency.getSymbol())).formatted(Formatting.YELLOW), true); + target.playSound(SoundEvents.BLOCK_BELL_USE, 1.0f, 1.2f); + target.playSound(SoundEvents.ENTITY_EXPERIENCE_ORB_PICKUP, 0.8f, 1.0f); + + return 1; + } + source.sendMessage(Text.literal(languageManager.getMessageRaw(source.getUuid(), "cme.apay.failed")).formatted(Formatting.RED)); + return 0; + } + + private int listRequests(CommandContext ctx) { + ServerPlayerEntity player = ctx.getSource().getPlayer(); + if (player == null) return 0; + + List requests = paymentRequestManager.getPendingRequests(player.getUuid()); + if (requests.isEmpty()) { + player.sendMessage(Text.literal(languageManager.getMessageRaw(player.getUuid(), "cme.requests.empty")).formatted(Formatting.YELLOW)); + return 0; + } + + player.sendMessage(Text.literal(languageManager.getMessageRaw(player.getUuid(), "cme.requests.list")).formatted(Formatting.AQUA)); + for (com.gvsds.tcme.model.PaymentRequest req : requests) { + Optional currency = currencyManager.getCurrency(req.getCurrencyId()); + String currencyName = currency.map(c -> c.getName()).orElse(String.valueOf(req.getCurrencyId())); + player.sendMessage(Text.literal(String.format(languageManager.getMessageRaw(player.getUuid(), "cme.requests.item"), req.getId(), req.getAmount(), currencyName)).formatted(Formatting.WHITE)); + } + return 1; + } + + private int acceptRequest(CommandContext ctx, int requestId) { + ServerPlayerEntity player = ctx.getSource().getPlayer(); + if (player == null) return 0; + + UUID fromUUID = null; + double amount = 0; + int currencyId = 0; + for (com.gvsds.tcme.model.PaymentRequest req : paymentRequestManager.getPendingRequests(player.getUuid())) { + if (req.getId() == requestId) { + fromUUID = req.getFromUUID(); + amount = req.getAmount(); + currencyId = req.getCurrencyId(); + break; + } + } + + if (paymentRequestManager.acceptRequest(requestId)) { + player.sendMessage(Text.literal(String.format(languageManager.getMessageRaw(player.getUuid(), "cme.requests.accepted"), requestId)).formatted(Formatting.GREEN)); + + if (fromUUID != null) { + ServerPlayerEntity fromPlayer = ctx.getSource().getServer().getPlayerManager().getPlayer(fromUUID); + if (fromPlayer != null) { + Optional currency = currencyManager.getCurrency(currencyId); + String currencySymbol = currency.map(com.gvsds.tcme.model.Currency::getSymbol).orElse("?"); + String fromName = player.getName().getString(); + + fromPlayer.sendMessage(Text.literal(String.format(languageManager.getMessageRaw(fromPlayer.getUuid(), "cme.requests.accepted_by"), fromName, requestId, amount, currencySymbol)).formatted(Formatting.GREEN)); + fromPlayer.sendMessage(Text.literal(String.format("§a§l%s §r§7已接受你的付款请求: §a%.2f %s", fromName, amount, currencySymbol)).formatted(Formatting.GREEN), true); + fromPlayer.playSound(SoundEvents.ENTITY_PLAYER_LEVELUP, 1.0f, 1.0f); + fromPlayer.playSound(SoundEvents.UI_TOAST_CHALLENGE_COMPLETE, 0.8f, 1.0f); + } + } + + return 1; + } + player.sendMessage(Text.literal(String.format(languageManager.getMessageRaw(player.getUuid(), "cme.requests.accept_failed"), requestId)).formatted(Formatting.RED)); + return 0; + } + + private int cancelRequest(CommandContext ctx, int requestId) { + ServerPlayerEntity player = ctx.getSource().getPlayer(); + if (player == null) return 0; + + Optional opt = paymentRequestManager.getRequestById(requestId); + if (opt.isEmpty() || !opt.get().getFromUUID().equals(player.getUuid())) { + player.sendMessage(Text.literal(languageManager.getMessageRaw(player.getUuid(), "cme.requests.not_yours")).formatted(Formatting.RED)); + return 0; + } + + com.gvsds.tcme.model.PaymentRequest req = opt.get(); + if (!"pending".equals(req.getStatus())) { + player.sendMessage(Text.literal(languageManager.getMessageRaw(player.getUuid(), "cme.requests.not_pending")).formatted(Formatting.RED)); + return 0; + } + + if (paymentRequestManager.cancelRequest(requestId)) { + player.sendMessage(Text.literal(String.format(languageManager.getMessageRaw(player.getUuid(), "cme.requests.revoked"), requestId)).formatted(Formatting.GREEN)); + + ServerPlayerEntity toPlayer = ctx.getSource().getServer().getPlayerManager().getPlayer(req.getToUUID()); + if (toPlayer != null) { + Optional currency = currencyManager.getCurrency(req.getCurrencyId()); + String currencySymbol = currency.map(com.gvsds.tcme.model.Currency::getSymbol).orElse("?"); + String fromName = player.getName().getString(); + + toPlayer.sendMessage(Text.literal(String.format(languageManager.getMessageRaw(toPlayer.getUuid(), "cme.requests.revoked_by"), fromName, requestId, req.getAmount(), currencySymbol)).formatted(Formatting.YELLOW)); + toPlayer.playSound(SoundEvents.BLOCK_BELL_USE, 1.0f, 0.8f); + } + + return 1; + } + player.sendMessage(Text.literal(String.format(languageManager.getMessageRaw(player.getUuid(), "cme.requests.revoke_failed"), requestId)).formatted(Formatting.RED)); + return 0; + } + + private int rejectRequest(CommandContext ctx, int requestId) { + ServerPlayerEntity player = ctx.getSource().getPlayer(); + if (player == null) return 0; + + Optional opt = paymentRequestManager.getRequestById(requestId); + if (opt.isEmpty() || !opt.get().getToUUID().equals(player.getUuid())) { + player.sendMessage(Text.literal(languageManager.getMessageRaw(player.getUuid(), "cme.requests.not_yours")).formatted(Formatting.RED)); + return 0; + } + + com.gvsds.tcme.model.PaymentRequest req = opt.get(); + if (!"pending".equals(req.getStatus())) { + player.sendMessage(Text.literal(languageManager.getMessageRaw(player.getUuid(), "cme.requests.not_pending")).formatted(Formatting.RED)); + return 0; + } + + if (paymentRequestManager.rejectRequest(requestId)) { + player.sendMessage(Text.literal(String.format(languageManager.getMessageRaw(player.getUuid(), "cme.requests.rejected"), requestId)).formatted(Formatting.GREEN)); + + ServerPlayerEntity fromPlayer = ctx.getSource().getServer().getPlayerManager().getPlayer(req.getFromUUID()); + if (fromPlayer != null) { + Optional currency = currencyManager.getCurrency(req.getCurrencyId()); + String currencySymbol = currency.map(com.gvsds.tcme.model.Currency::getSymbol).orElse("?"); + String toName = player.getName().getString(); + + fromPlayer.sendMessage(Text.literal(String.format(languageManager.getMessageRaw(fromPlayer.getUuid(), "cme.requests.rejected_by"), toName, requestId, req.getAmount(), currencySymbol)).formatted(Formatting.RED)); + fromPlayer.sendMessage(Text.literal(String.format("§c§l%s §r§7拒绝了你的付款请求: §c%.2f %s", toName, req.getAmount(), currencySymbol)).formatted(Formatting.RED), true); + fromPlayer.playSound(SoundEvents.ENTITY_VILLAGER_NO, 1.0f, 1.0f); + fromPlayer.playSound(SoundEvents.BLOCK_BELL_USE, 1.0f, 0.6f); + } + + return 1; + } + player.sendMessage(Text.literal(String.format(languageManager.getMessageRaw(player.getUuid(), "cme.requests.reject_failed"), requestId)).formatted(Formatting.RED)); + return 0; + } + + private int showWallet(CommandContext ctx) { + ServerPlayerEntity player = ctx.getSource().getPlayer(); + if (player == null) return 0; + + List currencies = currencyManager.getAllCurrencies(); + if (currencies.isEmpty()) { + player.sendMessage(Text.literal(languageManager.getMessageRaw(player.getUuid(), "cme.currency.no_currencies")).formatted(Formatting.YELLOW)); + return 0; + } + + player.sendMessage(Text.literal(languageManager.getMessageRaw(player.getUuid(), "cme.wallet.title")).formatted(Formatting.AQUA)); + boolean hasBalance = false; + for (com.gvsds.tcme.model.Currency c : currencies) { + double balance = balanceManager.getBalance(player.getUuid(), c.getId()); + if (balance > 0) { + hasBalance = true; + String msg = String.format(languageManager.getMessageRaw(player.getUuid(), "cme.wallet.balance"), c.getName(), balance, c.getSymbol()); + player.sendMessage(Text.literal(msg).formatted(Formatting.WHITE)); + } + } + if (!hasBalance) { + player.sendMessage(Text.literal(languageManager.getMessageRaw(player.getUuid(), "cme.wallet.empty")).formatted(Formatting.YELLOW)); + } + return 1; + } + + private int showLanguageList(CommandContext ctx) { + ServerPlayerEntity player = ctx.getSource().getPlayer(); + if (player == null) return 0; + player.sendMessage(Text.literal(String.format(languageManager.getMessageRaw(player.getUuid(), "cme.lang.available"), languageManager.getAvailableLanguages())).formatted(Formatting.AQUA)); + player.sendMessage(Text.literal(String.format(languageManager.getMessageRaw(player.getUuid(), "cme.lang.usage"))).formatted(Formatting.GRAY)); + return 1; + } + + private int setLanguage(CommandContext ctx, String language) { + ServerPlayerEntity player = ctx.getSource().getPlayer(); + if (player == null) return 0; + + language = language.toLowerCase(); + if (!languageManager.hasLanguage(language)) { + player.sendMessage(Text.literal(String.format(languageManager.getMessageRaw(player.getUuid(), "cme.lang.notfound"), language)).formatted(Formatting.RED)); + return 0; + } + + languageManager.setPlayerLanguage(player.getUuid(), language); + playerSettingsManager.setLanguage(player.getUuid(), language); + + player.sendMessage(Text.literal(String.format(languageManager.getMessageRaw(player.getUuid(), "cme.lang.changed"), language)).formatted(Formatting.GREEN)); + return 1; + } + + private int setExchangeRate(CommandContext ctx, String currencyName, double rate) { + Optional currency = currencyManager.getCurrencyByName(currencyName); + if (currency.isEmpty()) { + sendFeedback(ctx.getSource(), "cme.currency.not_found", currencyName); + return 0; + } + exchangeRateManager.setRate(currency.get().getId(), rate); + sendFeedback(ctx.getSource(), "cme.exchange_rate.set", currencyName, rate); + return 1; + } + + private int listExchangeRates(CommandContext ctx) { + List currencies = currencyManager.getAllCurrencies(); + if (currencies.isEmpty()) { + sendFeedback(ctx.getSource(), "cme.currency.no_currencies"); + return 0; + } + sendFeedback(ctx.getSource(), "cme.exchange_rate.list"); + for (com.gvsds.tcme.model.Currency c : currencies) { + double rate = exchangeRateManager.getRate(c.getId()); + sendFeedback(ctx.getSource(), "cme.exchange_rate.item", c.getName(), rate); + } + return 1; + } + + private int previewConvert(CommandContext ctx, double amount, String fromCurrency, String toCurrency) { + ServerPlayerEntity player = ctx.getSource().getPlayer(); + if (player == null) return 0; + + if (amount <= 0) { + player.sendMessage(Text.literal(languageManager.getMessageRaw(player.getUuid(), "cme.convert.invalid_amount")).formatted(Formatting.RED)); + return 0; + } + + Optional fromOpt = currencyManager.getCurrencyByName(fromCurrency); + Optional toOpt = currencyManager.getCurrencyByName(toCurrency); + + if (fromOpt.isEmpty()) { + player.sendMessage(Text.literal(String.format(languageManager.getMessageRaw(player.getUuid(), "cme.currency.not_found"), fromCurrency)).formatted(Formatting.RED)); + return 0; + } + if (toOpt.isEmpty()) { + player.sendMessage(Text.literal(String.format(languageManager.getMessageRaw(player.getUuid(), "cme.currency.not_found"), toCurrency)).formatted(Formatting.RED)); + return 0; + } + + com.gvsds.tcme.model.Currency from = fromOpt.get(); + com.gvsds.tcme.model.Currency to = toOpt.get(); + + if (from.getId() == to.getId()) { + player.sendMessage(Text.literal(languageManager.getMessageRaw(player.getUuid(), "cme.convert.same_currency")).formatted(Formatting.RED)); + return 0; + } + + double fromRate = exchangeRateManager.getRate(from.getId()); + double toRate = exchangeRateManager.getRate(to.getId()); + double convertedAmount = exchangeRateManager.convert(amount, from.getId(), to.getId()); + double currentBalance = balanceManager.getBalance(player.getUuid(), from.getId()); + + if (currentBalance < amount) { + player.sendMessage(Text.literal(String.format(languageManager.getMessageRaw(player.getUuid(), "cme.convert.insufficient"), currentBalance, from.getSymbol(), amount)).formatted(Formatting.RED)); + return 0; + } + + player.sendMessage(Text.literal(languageManager.getMessageRaw(player.getUuid(), "cme.convert.title")).formatted(Formatting.AQUA)); + player.sendMessage(Text.literal(String.format(languageManager.getMessageRaw(player.getUuid(), "cme.convert.from"), amount, from.getName(), from.getSymbol())).formatted(Formatting.WHITE)); + player.sendMessage(Text.literal(String.format(languageManager.getMessageRaw(player.getUuid(), "cme.convert.to"), String.format("%.2f", convertedAmount), to.getName(), to.getSymbol())).formatted(Formatting.GREEN)); + player.sendMessage(Text.literal(String.format(languageManager.getMessageRaw(player.getUuid(), "cme.convert.rate"), from.getName(), to.getName(), fromRate, toRate)).formatted(Formatting.GRAY)); + player.sendMessage(Text.literal(String.format(languageManager.getMessageRaw(player.getUuid(), "cme.convert.balance_from"), currentBalance, from.getSymbol())).formatted(Formatting.GRAY)); + player.sendMessage(Text.literal(String.format(languageManager.getMessageRaw(player.getUuid(), "cme.convert.balance_to"), String.format("%.2f", balanceManager.getBalance(player.getUuid(), to.getId()) + convertedAmount), to.getSymbol())).formatted(Formatting.GRAY)); + player.sendMessage(Text.literal(String.format(languageManager.getMessageRaw(player.getUuid(), "cme.convert.prompt"), amount, fromCurrency, toCurrency)).formatted(Formatting.YELLOW)); + return 1; + } + + private int executeConvert(CommandContext ctx, double amount, String fromCurrency, String toCurrency) { + ServerPlayerEntity player = ctx.getSource().getPlayer(); + if (player == null) return 0; + + if (amount <= 0) { + player.sendMessage(Text.literal(languageManager.getMessageRaw(player.getUuid(), "cme.convert.invalid_amount")).formatted(Formatting.RED)); + return 0; + } + + Optional fromOpt = currencyManager.getCurrencyByName(fromCurrency); + Optional toOpt = currencyManager.getCurrencyByName(toCurrency); + + if (fromOpt.isEmpty()) { + player.sendMessage(Text.literal(String.format(languageManager.getMessageRaw(player.getUuid(), "cme.currency.not_found"), fromCurrency)).formatted(Formatting.RED)); + return 0; + } + if (toOpt.isEmpty()) { + player.sendMessage(Text.literal(String.format(languageManager.getMessageRaw(player.getUuid(), "cme.currency.not_found"), toCurrency)).formatted(Formatting.RED)); + return 0; + } + + com.gvsds.tcme.model.Currency from = fromOpt.get(); + com.gvsds.tcme.model.Currency to = toOpt.get(); + + if (from.getId() == to.getId()) { + player.sendMessage(Text.literal(languageManager.getMessageRaw(player.getUuid(), "cme.convert.same_currency")).formatted(Formatting.RED)); + return 0; + } + + double convertedAmount = exchangeRateManager.convert(amount, from.getId(), to.getId()); + double currentBalance = balanceManager.getBalance(player.getUuid(), from.getId()); + + if (currentBalance < amount) { + player.sendMessage(Text.literal(String.format(languageManager.getMessageRaw(player.getUuid(), "cme.convert.insufficient"), currentBalance, from.getSymbol(), amount)).formatted(Formatting.RED)); + return 0; + } + + boolean withdrawn = balanceManager.subtractBalance(player.getUuid(), from.getId(), amount); + if (!withdrawn) { + player.sendMessage(Text.literal(languageManager.getMessageRaw(player.getUuid(), "cme.convert.failed")).formatted(Formatting.RED)); + return 0; + } + + boolean deposited = balanceManager.addBalance(player.getUuid(), to.getId(), convertedAmount); + if (!deposited) { + balanceManager.addBalance(player.getUuid(), from.getId(), amount); + player.sendMessage(Text.literal(languageManager.getMessageRaw(player.getUuid(), "cme.convert.refunded")).formatted(Formatting.RED)); + return 0; + } + + player.sendMessage(Text.literal(String.format(languageManager.getMessageRaw(player.getUuid(), "cme.convert.success"), amount, from.getSymbol(), String.format("%.2f", convertedAmount), to.getSymbol())).formatted(Formatting.GREEN)); + return 1; + } + + private int demoAlert(CommandContext ctx) { + ServerPlayerEntity player = ctx.getSource().getPlayer(); + if (player == null) return 0; + + ServerGuiAPI.showAlert(player, "§6§l[Alert Demo]", + "§e这是一个警告提示框示例!\n§7This is an alert demo!"); + sendFeedback(ctx.getSource(), "cme.uidemo.alert", player.getName().getString()); + return 1; + } + + private int demoConfirm(CommandContext ctx) { + ServerPlayerEntity player = ctx.getSource().getPlayer(); + if (player == null) return 0; + + ServerGuiAPI.showConfirm(player, "§6§l[Confirm Demo]", + "§e你确定要执行此操作吗?\n§7Are you sure?", + (confirmed) -> { + if (confirmed) { + player.sendMessage(Text.literal("§a[GUI] 你点击了确认!You confirmed!").formatted(Formatting.GREEN)); + } else { + player.sendMessage(Text.literal("§c[GUI] 你点击了取消!You cancelled!").formatted(Formatting.RED)); + } + }); + sendFeedback(ctx.getSource(), "cme.uidemo.confirm", player.getName().getString()); + return 1; + } + + private int demoInput(CommandContext ctx) { + ServerPlayerEntity player = ctx.getSource().getPlayer(); + if (player == null) return 0; + + ServerGuiAPI.showInputBox(player, "§6§l[Input Demo]", + "§e请输入内容... / Enter text:", + (input) -> { + if (input != null) { + player.sendMessage(Text.literal("§a[GUI] 你输入了: " + input + " / You entered: " + input).formatted(Formatting.GREEN)); + } else { + player.sendMessage(Text.literal("§c[GUI] 你取消了输入!You cancelled!").formatted(Formatting.RED)); + } + }); + sendFeedback(ctx.getSource(), "cme.uidemo.input", player.getName().getString()); + return 1; + } + + private int demoSmallChest(CommandContext ctx) { + ServerPlayerEntity player = ctx.getSource().getPlayer(); + if (player == null) return 0; + + ServerGuiAPI.showSmallChest(player, "§6§l[Small Chest Demo]", + (slot) -> { + player.sendMessage(Text.literal("§a[GUI] 你点击了槽位 / You clicked slot: " + slot).formatted(Formatting.GREEN)); + }); + sendFeedback(ctx.getSource(), "cme.uidemo.smallchest", player.getName().getString()); + return 1; + } + + private int demoMediumChest(CommandContext ctx) { + ServerPlayerEntity player = ctx.getSource().getPlayer(); + if (player == null) return 0; + + ServerGuiAPI.showMediumChest(player, "§6§l[Medium Chest Demo]", + (slot) -> { + player.sendMessage(Text.literal("§a[GUI] 你点击了槽位 / You clicked slot: " + slot).formatted(Formatting.GREEN)); + }); + sendFeedback(ctx.getSource(), "cme.uidemo.mediumchest", player.getName().getString()); + return 1; + } + + private int demoLargeChest(CommandContext ctx) { + ServerPlayerEntity player = ctx.getSource().getPlayer(); + if (player == null) return 0; + + ServerGuiAPI.showLargeChest(player, "§6§l[Large Chest Demo]", + (slot) -> { + player.sendMessage(Text.literal("§a[GUI] 你点击了槽位 / You clicked slot: " + slot).formatted(Formatting.GREEN)); + }); + sendFeedback(ctx.getSource(), "cme.uidemo.largechest", player.getName().getString()); + return 1; + } + + private int demoAnvil(CommandContext ctx) { + ServerPlayerEntity player = ctx.getSource().getPlayer(); + if (player == null) return 0; + + ServerGuiAPI.showAnvil(player, "§6§l[Anvil Demo]"); + sendFeedback(ctx.getSource(), "cme.uidemo.anvil", player.getName().getString()); + return 1; + } + + private BlockPos getTargetedBlockPos(ServerCommandSource source) { + ServerPlayerEntity player = source.getPlayer(); + if (player == null) return null; + ServerWorld world = source.getWorld(); + RaycastContext context = new RaycastContext( + player.getCameraPosVec(1.0f), + player.getCameraPosVec(1.0f).add(player.getRotationVec(1.0f).multiply(5.0)), + RaycastContext.ShapeType.OUTLINE, + RaycastContext.FluidHandling.NONE, + player + ); + BlockHitResult hitResult = world.raycast(context); + if (hitResult.getType() == HitResult.Type.BLOCK) { + return hitResult.getBlockPos(); + } + return null; + } + + private int meteoRaycast(CommandContext ctx) { + BlockPos pos = getTargetedBlockPos(ctx.getSource()); + if (pos == null) { + ctx.getSource().sendFeedback(() -> Text.literal("§c请对准一个方块!").formatted(Formatting.RED), false); + return 0; + } + return meteo(ctx, pos.getX(), pos.getY(), pos.getZ()); + } + + private int meteo(CommandContext ctx, int x, int y, int z) { + ServerCommandSource source = ctx.getSource(); + ServerWorld world = source.getWorld(); + + com.gvsds.tcme.CardinalCooperativeCapitalismMarketEconomy.LOGGER.info("[METEO] Creating MeteoSession at x={}, y={}, z={}", x, y, z); + MeteoSession session = new MeteoSession(world, x, y, z); + MeteoSessions.add(session); + + source.sendFeedback(() -> Text.literal(String.format("§6§l[METEO#%d] §e陨石袭击已启动!目标: %d, %d, %d", session.getId(), x, y, z)).formatted(Formatting.GOLD), false); + + return 1; + } + + private int lineRaycast(CommandContext ctx, int range) { + BlockPos pos = getTargetedBlockPos(ctx.getSource()); + if (pos == null) { + ctx.getSource().sendFeedback(() -> Text.literal("§c请对准一个方块!").formatted(Formatting.RED), false); + return 0; + } + return line(ctx, pos.getX(), pos.getZ(), range); + } + + private int line(CommandContext ctx, int x, int z, int range) { + ServerCommandSource source = ctx.getSource(); + ServerWorld world = source.getWorld(); + + int topY = world.getTopY(Heightmap.Type.WORLD_SURFACE, x, z); + com.gvsds.tcme.CardinalCooperativeCapitalismMarketEconomy.LOGGER.info("[LINE] Creating LineSession at x={}, z={}, topY={}, range={}", x, z, topY, range); + LineSession session = new LineSession(world, x, z, topY, range); + LineSessions.add(session); + + source.sendFeedback(() -> Text.literal(String.format("§b§l[LINE#%d] §e波纹扩散已启动!中心: %d, %d, %d | 范围: %d", session.getId(), x, topY, z, range)).formatted(Formatting.AQUA), false); + + return 1; + } + + private int boomRaycast(CommandContext ctx, int range, int depth) { + BlockPos pos = getTargetedBlockPos(ctx.getSource()); + if (pos == null) { + ctx.getSource().sendFeedback(() -> Text.literal("§c请对准一个方块!").formatted(Formatting.RED), false); + return 0; + } + return boom(ctx, pos.getX(), pos.getY(), pos.getZ(), range, depth); + } + + private int boom(CommandContext ctx, int x, int y, int z, int range, int depth) { + ServerCommandSource source = ctx.getSource(); + ServerWorld world = source.getWorld(); + + com.gvsds.tcme.CardinalCooperativeCapitalismMarketEconomy.LOGGER.info("[BOOM] Creating BoomSession at x={}, y={}, z={}, range={}, depth={}", x, y, z, range, depth); + BoomSession session = new BoomSession(world, x, y, z, range, depth, false); + BoomSessions.add(session); + + source.sendFeedback(() -> Text.literal(String.format("§c§l[BOOM#%d] §e引爆序列已启动!坐标: %d, %d, %d | 范围: %d | 深度: %s", session.getId(), x, y, z, range, depth == -1 ? "基岩层" : String.valueOf(depth))).formatted(Formatting.RED), false); + + return 1; + } + + private int sqDemoRaycast(CommandContext ctx) { + BlockPos pos = getTargetedBlockPos(ctx.getSource()); + if (pos == null) { + ctx.getSource().sendFeedback(() -> Text.literal("§c请对准一个方块!").formatted(Formatting.RED), false); + return 0; + } + return sqDemo(ctx, pos.getX(), pos.getY(), pos.getZ(), 10); + } + + private int sqDemo(CommandContext ctx, int x, int y, int z, int range) { + ServerCommandSource source = ctx.getSource(); + ServerWorld world = source.getWorld(); + + BoomSession session = new BoomSession(world, x, y, z, range, 5, true); + BoomSessions.add(session); + + source.sendFeedback(() -> Text.literal(String.format("§8§l[SQ#%d] §e球体演示启动!坐标: %d, %d, %d | 范围: %d", session.getId(), x, y, z, range)).formatted(Formatting.DARK_GRAY), false); + + return 1; + } + + private int listTestSessions(CommandContext ctx) { + ServerCommandSource source = ctx.getSource(); + List all = new ArrayList<>(); + all.addAll(BoomSessions); + all.addAll(MeteoSessions); + all.addAll(LineSessions); + + if (all.isEmpty()) { + source.sendFeedback(() -> Text.literal("§7当前没有活动的 test 事件").formatted(Formatting.GRAY), false); + return 0; + } + + source.sendFeedback(() -> Text.literal(String.format("§6§l=== 活动事件列表 (%d) ===", all.size())).formatted(Formatting.GOLD), false); + for (TestSession s : all) { + source.sendFeedback(() -> Text.literal(s.getDescription()).formatted(Formatting.WHITE), false); + } + return all.size(); + } + + private int cancelTestSession(CommandContext ctx, int id) { + ServerCommandSource source = ctx.getSource(); + + TestSession target = null; + for (BoomSession s : BoomSessions) { + if (s.getId() == id) { target = s; break; } + } + if (target == null) { + for (MeteoSession s : MeteoSessions) { + if (s.getId() == id) { target = s; break; } + } + } + if (target == null) { + for (LineSession s : LineSessions) { + if (s.getId() == id) { target = s; break; } + } + } + + if (target == null) { + source.sendFeedback(() -> Text.literal(String.format("§c未找到 ID 为 %d 的活动事件", id)).formatted(Formatting.RED), false); + return 0; + } + + final TestSession finalTarget = target; + source.sendFeedback(() -> Text.literal(String.format("§e正在取消事件 #%d [%s]...", id, finalTarget.getType())).formatted(Formatting.YELLOW), false); + target.cancel(); + source.sendFeedback(() -> Text.literal(String.format("§a已取消事件 #%d [%s]", id, finalTarget.getType())).formatted(Formatting.GREEN), false); + return 1; + } + + private static class BoomSession implements TestSession { + private static final int TOTAL_TICKS = 1200; + private static final int BLACK_SPHERE_GROW_END = 400; + private static final int BLACK_SPHERE_MERGE_END = 500; + private static final int BLACK_SPHERE_SHRINK_END = 600; + private static final int POST_EXPLOSION_DURATION = 400; + private static final int DEMO_CLEANUP_DURATION = 60; + + private final int id = NextTestId.getAndIncrement(); + private final ServerWorld world; + private final int x; + private final int z; + private final double cx; + private final double cz; + private final int range; + private final int depth; + private final double topY; + private int tickCounter = 0; + private boolean finished = false; + private boolean cancelled = false; + private final ServerBossBar bossBar; + private boolean explosionExecuted = false; + private int explosionExecutedTick = -1; + private final boolean demoOnly; + + private final double maxSphereRadius; + private final double sphereCenterY; + private final Set allBuiltPositions = new HashSet<>(); + private final Set lastCenterSphereBlocks = new HashSet<>(); + private final List> lastSmallSphereBlocks = new ArrayList<>(); + private int lastCenterRadius = -1; + private boolean blocksCleared = false; + + BoomSession(ServerWorld world, int x, int y, int z, int range, int depth, boolean demoOnly) { + this.world = world; + this.x = x; + this.z = z; + this.cx = x + 0.5; + this.cz = z + 0.5; + this.range = range; + this.depth = depth; + this.topY = y; + this.demoOnly = demoOnly; + this.maxSphereRadius = Math.min(range * 0.5, 20.0); + this.sphereCenterY = topY; + String prefix = demoOnly ? "§8§l[SQ] §e" : "§c§l[BOOM] §e"; + this.bossBar = new ServerBossBar( + Text.literal(prefix + "初始化中..."), + BossBar.Color.YELLOW, + BossBar.Style.NOTCHED_20 + ); + this.bossBar.setVisible(true); + this.bossBar.setPercent(1.0f); + var players = world.getServer().getPlayerManager().getPlayerList(); + com.gvsds.tcme.CardinalCooperativeCapitalismMarketEconomy.LOGGER.info("[BOOM] BossBar created, adding {} players", players.size()); + for (ServerPlayerEntity player : players) { + this.bossBar.addPlayer(player); + } + for (int i = 0; i < 6; i++) { + lastSmallSphereBlocks.add(new HashSet<>()); + } + updateBossBar(); + } + + boolean isFinished() { + return finished; + } + + @Override + public int getId() { + return id; + } + + @Override + public String getType() { + return demoOnly ? "SQ" : "BOOM"; + } + + @Override + public String getDescription() { + String worldName = world.getRegistryKey().getValue().toString(); + if (demoOnly) { + return String.format("§8[SQ#%d] §7世界: %s §7| 坐标: %d, %d, %d §7| 范围: %d §7| tick: %d/%d", + id, worldName, x, (int) topY, z, range, tickCounter, BLACK_SPHERE_SHRINK_END + DEMO_CLEANUP_DURATION); + } + String phase; + if (explosionExecuted) { + phase = "爆炸后"; + } else if (tickCounter < BLACK_SPHERE_GROW_END) { + phase = "核心凝聚"; + } else if (tickCounter < BLACK_SPHERE_MERGE_END) { + phase = "核心合并"; + } else if (tickCounter < BLACK_SPHERE_SHRINK_END) { + phase = "核心坍缩"; + } else { + phase = "爆发准备"; + } + return String.format("§c[BOOM#%d] §7世界: %s §7| 坐标: %d, %d, %d §7| 范围: %d §7| 深度: %s §7| 阶段: %s §7| tick: %d/%d", + id, worldName, x, (int) topY, z, range, depth == -1 ? "基岩层" : String.valueOf(depth), phase, tickCounter, TOTAL_TICKS); + } + + @Override + public void cancel() { + if (finished) return; + cancelled = true; + if (!blocksCleared) { + clearAllSphereBlocks(); + } + restoreRemainingBlocks(false); + bossBar.clearPlayers(); + bossBar.setVisible(false); + finished = true; + } + + private void restoreRemainingBlocks(boolean checkExplosionRange) { + int centerTopY = (int) topY; + int upwardHeight = Math.max(5, range / 2); + int bottomY = (depth == -1) ? world.getBottomY() + 1 : (centerTopY - depth); + double rangeSq = (double) range * (double) range; + + for (BlockPos pos : allBuiltPositions) { + boolean shouldRestore = GlobalBlockRegistry.release(world, pos); + if (!shouldRestore) continue; + + boolean skip = false; + if (checkExplosionRange) { + int dx = pos.getX() - x; + int dz = pos.getZ() - z; + boolean inRange = (dx * dx + dz * dz) <= rangeSq; + boolean inY = pos.getY() >= bottomY && pos.getY() <= (centerTopY + upwardHeight); + skip = inRange && inY; + } + + if (!skip) { + BlockState original = GlobalBlockRegistry.getOriginalState(world, pos); + if (original != null) { + world.setBlockState(pos, original, RESTORE_FLAGS); + NbtCompound nbt = GlobalBlockRegistry.getOriginalNbt(world, pos); + if (nbt != null) { + BlockEntity newBe = BlockEntity.createFromNbt(pos, original, nbt, world.getRegistryManager()); + if (newBe != null) world.addBlockEntity(newBe); + } + } else { + world.setBlockState(pos, Blocks.AIR.getDefaultState(), RESTORE_FLAGS); + } + } + GlobalBlockRegistry.clearEntry(world, pos); + } + allBuiltPositions.clear(); + } + + void tick() { + if (finished) return; + int t = tickCounter; + + if (demoOnly) { + if (t >= BLACK_SPHERE_SHRINK_END + DEMO_CLEANUP_DURATION) { + bossBar.clearPlayers(); + bossBar.setVisible(false); + finished = true; + } else if (t >= BLACK_SPHERE_SHRINK_END) { + if (t == BLACK_SPHERE_SHRINK_END) { + clearAllSphereBlocks(); + restoreAllRecordedBlocks(); + } + updateBossBarDemo(t); + } else { + updateBossBar(); + if (t < BLACK_SPHERE_GROW_END) { + tickBlackSphereGrow(t); + } else if (t < BLACK_SPHERE_MERGE_END) { + tickBlackSphereMerge(t); + } else { + tickBlackSphereShrink(t); + } + } + tickCounter++; + return; + } + + if (explosionExecuted) { + updateBossBarPostExplosion(); + if (t >= explosionExecutedTick + POST_EXPLOSION_DURATION) { + bossBar.clearPlayers(); + bossBar.setVisible(false); + finished = true; + } + tickCounter++; + return; + } + + updateBossBar(); + + if (t >= TOTAL_TICKS) { + executeFinalExplosion(); + explosionExecuted = true; + explosionExecutedTick = t; + tickCounter++; + return; + } + + if (t < BLACK_SPHERE_GROW_END) { + tickBlackSphereGrow(t); + } else if (t < BLACK_SPHERE_MERGE_END) { + tickBlackSphereMerge(t); + } else if (t < BLACK_SPHERE_SHRINK_END) { + tickBlackSphereShrink(t); + } else { + tickIntensePhase(t); + } + + tickCounter++; + } + + private void updateBossBar() { + int remainingTicks = Math.max(0, TOTAL_TICKS - tickCounter); + int remainingSeconds = remainingTicks / 20; + int minutes = remainingSeconds / 60; + int seconds = remainingSeconds % 60; + String worldName = world.getRegistryKey().getValue().toString(); + + String phaseName; + BossBar.Color color; + if (tickCounter < BLACK_SPHERE_GROW_END) { + phaseName = "§8● 核心凝聚"; + color = BossBar.Color.PURPLE; + } else if (tickCounter < BLACK_SPHERE_MERGE_END) { + phaseName = "§8● 核心合并"; + color = BossBar.Color.PURPLE; + } else if (tickCounter < BLACK_SPHERE_SHRINK_END) { + phaseName = "§8● 核心坍缩"; + color = BossBar.Color.PINK; + } else { + phaseName = "§c☠ 爆发准备"; + color = BossBar.Color.RED; + } + + Text title = Text.literal(String.format("§c§l[BOOM] §e世界: %s §7| §e坐标: %d, %d §7| %s §7| §c倒计时: %02d:%02d", + worldName, x, z, phaseName, minutes, seconds)); + this.bossBar.setName(title); + + float progress = 1.0f - ((float) tickCounter / TOTAL_TICKS); + this.bossBar.setPercent(Math.max(0.0f, Math.min(1.0f, progress))); + this.bossBar.setColor(color); + + refreshBossBarPlayers(); + } + + private void updateBossBarPostExplosion() { + int elapsed = tickCounter - explosionExecutedTick; + int remainingTicks = Math.max(0, POST_EXPLOSION_DURATION - elapsed); + int remainingSeconds = remainingTicks / 20; + String worldName = world.getRegistryKey().getValue().toString(); + + Text title = Text.literal(String.format("§a§l[BOOM] §e世界: %s §7| §e坐标: %d, %d §7| §a爆炸完成 §7| §eHUD消失: %ds", + worldName, x, z, remainingSeconds)); + this.bossBar.setName(title); + + float progress = (float) elapsed / POST_EXPLOSION_DURATION; + this.bossBar.setPercent(Math.max(0.0f, 1.0f - progress)); + this.bossBar.setColor(BossBar.Color.GREEN); + + refreshBossBarPlayers(); + } + + private void updateBossBarDemo(int t) { + int elapsed = t - BLACK_SPHERE_SHRINK_END; + int remainingTicks = Math.max(0, DEMO_CLEANUP_DURATION - elapsed); + int remainingSeconds = remainingTicks / 20; + String worldName = world.getRegistryKey().getValue().toString(); + + Text title = Text.literal(String.format("§8§l[SQ] §e世界: %s §7| §e坐标: %d, %d §7| §a演示完成 §7| §eHUD消失: %ds", + worldName, x, z, remainingSeconds)); + this.bossBar.setName(title); + + float progress = (float) elapsed / DEMO_CLEANUP_DURATION; + this.bossBar.setPercent(Math.max(0.0f, 1.0f - progress)); + this.bossBar.setColor(BossBar.Color.GREEN); + + refreshBossBarPlayers(); + } + + private void refreshBossBarPlayers() { + if (tickCounter % 10 == 0) { + var playerList = world.getServer().getPlayerManager().getPlayerList(); + var existingPlayers = bossBar.getPlayers(); + java.util.Set existingUUIDs = new java.util.HashSet<>(); + for (ServerPlayerEntity p : existingPlayers) { + existingUUIDs.add(p.getUuid()); + } + for (ServerPlayerEntity player : playerList) { + if (!existingUUIDs.contains(player.getUuid())) { + bossBar.addPlayer(player); + } + } + } + } + + private void playSoundToAll(double x, double y, double z, net.minecraft.sound.SoundEvent sound, net.minecraft.sound.SoundCategory category, float volume, float pitch) { + world.playSound(null, x, y, z, sound, category, 10000.0f, pitch); + } + + private void tickBlackSphereGrow(int t) { + double progress = (double) t / BLACK_SPHERE_GROW_END; + int currentRadius = (int) Math.round(progress * maxSphereRadius); + + if (currentRadius != lastCenterRadius) { + if (!lastCenterSphereBlocks.isEmpty()) { + clearSphereBlocks(lastCenterSphereBlocks); + lastCenterSphereBlocks.clear(); + } + if (currentRadius > 0) { + buildSphere((int) cx, (int) sphereCenterY, (int) cz, currentRadius, lastCenterSphereBlocks); + } + lastCenterRadius = currentRadius; + } + + if (t % 5 == 0) { + double initialDistance = maxSphereRadius * 2.0; + double currentDistance = initialDistance - progress * (initialDistance - maxSphereRadius); + int smallRadius = Math.max(1, (int) Math.round(maxSphereRadius * 0.2)); + double rotationAngle = progress * Math.PI * 4; + + for (int i = 0; i < 6; i++) { + if (!lastSmallSphereBlocks.get(i).isEmpty()) { + clearSphereBlocks(lastSmallSphereBlocks.get(i)); + lastSmallSphereBlocks.get(i).clear(); + } + + double angle = (Math.PI * 2 * i) / 6.0 + rotationAngle; + double sx = cx + Math.cos(angle) * currentDistance; + double sz = cz + Math.sin(angle) * currentDistance; + double sy = sphereCenterY; + + buildSphere((int) Math.round(sx), (int) Math.round(sy), (int) Math.round(sz), smallRadius, lastSmallSphereBlocks.get(i)); + } + } + + if (t % 2 == 0) { + spawnBlackSphereParticles(); + } + + if (t % 40 == 0) { + playSoundToAll(cx, sphereCenterY, cz, SoundEvents.BLOCK_END_PORTAL_SPAWN, SoundCategory.BLOCKS, 1.0f, 0.5f); + } + } + + private void tickBlackSphereMerge(int t) { + int relativeT = t - BLACK_SPHERE_GROW_END; + int duration = BLACK_SPHERE_MERGE_END - BLACK_SPHERE_GROW_END; + double progress = (double) relativeT / duration; + + if (relativeT % 5 != 0) { + if (t % 2 == 0) { + spawnBlackSphereParticles(); + double mergeDistance = maxSphereRadius * (1.0 - progress); + double mergeRotation = Math.PI * 4 + progress * Math.PI * 3; + for (int i = 0; i < 6; i++) { + double angle = (Math.PI * 2 * i) / 6.0 + mergeRotation; + double sx = cx + Math.cos(angle) * mergeDistance; + double sz = cz + Math.sin(angle) * mergeDistance; + spawnBlackDripParticles(sx, sphereCenterY, sz, cx, sphereCenterY, cz); + } + } + return; + } + + for (int i = 0; i < 6; i++) { + if (!lastSmallSphereBlocks.get(i).isEmpty()) { + clearSphereBlocks(lastSmallSphereBlocks.get(i)); + lastSmallSphereBlocks.get(i).clear(); + } + } + + int centerRadius = (int) Math.round(maxSphereRadius + progress * 2); + if (centerRadius != lastCenterRadius) { + if (!lastCenterSphereBlocks.isEmpty()) { + clearSphereBlocks(lastCenterSphereBlocks); + lastCenterSphereBlocks.clear(); + } + if (centerRadius > 0) { + buildSphere((int) cx, (int) sphereCenterY, (int) cz, centerRadius, lastCenterSphereBlocks); + } + lastCenterRadius = centerRadius; + } + + double mergeDistance = maxSphereRadius * (1.0 - progress); + int baseSmallRadius = Math.max(1, (int) Math.round(maxSphereRadius * 0.2)); + int smallRadius = (int) Math.max(1, Math.round(baseSmallRadius * (1.0 - progress))); + double mergeRotation = Math.PI * 4 + progress * Math.PI * 3; + + for (int i = 0; i < 6; i++) { + double angle = (Math.PI * 2 * i) / 6.0 + mergeRotation; + double sx = cx + Math.cos(angle) * mergeDistance; + double sz = cz + Math.sin(angle) * mergeDistance; + double sy = sphereCenterY; + + if (smallRadius > 0 && mergeDistance > 0.5) { + buildSphere((int) Math.round(sx), (int) Math.round(sy), (int) Math.round(sz), smallRadius, lastSmallSphereBlocks.get(i)); + } + + spawnBlackDripParticles(sx, sy, sz, cx, sphereCenterY, cz); + } + + spawnBlackSphereParticles(); + + if (relativeT % 20 == 0) { + playSoundToAll(cx, sphereCenterY, cz, SoundEvents.ENTITY_ENDERMAN_TELEPORT, SoundCategory.HOSTILE, 1.0f, 0.5f); + } + } + + private void tickBlackSphereShrink(int t) { + int relativeT = t - BLACK_SPHERE_MERGE_END; + int duration = BLACK_SPHERE_SHRINK_END - BLACK_SPHERE_MERGE_END; + double progress = (double) relativeT / duration; + + if (relativeT % 5 != 0) { + if (t % 2 == 0) { + spawnBlackSphereParticles(); + } + return; + } + + int currentRadius = (int) Math.round((maxSphereRadius + 2) * (1.0 - progress)); + if (currentRadius != lastCenterRadius) { + if (!lastCenterSphereBlocks.isEmpty()) { + clearSphereBlocks(lastCenterSphereBlocks); + lastCenterSphereBlocks.clear(); + } + if (currentRadius > 0) { + buildSphere((int) cx, (int) sphereCenterY, (int) cz, currentRadius, lastCenterSphereBlocks); + } + lastCenterRadius = currentRadius; + } + + spawnBlackSphereParticles(); + + if (relativeT % 20 == 0) { + playSoundToAll(cx, sphereCenterY, cz, SoundEvents.BLOCK_END_PORTAL_FRAME_FILL, SoundCategory.BLOCKS, 1.0f, 0.8f); + } + } + + private void tickIntensePhase(int t) { + int relativeT = t - BLACK_SPHERE_SHRINK_END; + + if (relativeT == 0) { + clearAllSphereBlocks(); + } + + if (t % 2 == 0) { + spawnBurstParticles(); + } + if (t % 10 == 0) { + spawnRangeBorder(); + summonRandomLightningInArea(); + } + if (range >= 8 && t % 30 == 0) { + spawnRandomSmallExplosion(); + } + } + + private static final int SKIP_DROPS_FLAGS = Block.SKIP_DROPS | Block.MOVED; + private static final int BUILD_FLAGS = SKIP_DROPS_FLAGS | Block.NOTIFY_LISTENERS; + private static final int RESTORE_FLAGS = Block.SKIP_DROPS | Block.NOTIFY_LISTENERS; + + private void buildSphere(int centerX, int centerY, int centerZ, int radius, Set blockSet) { + for (int dx = -radius; dx <= radius; dx++) { + for (int dy = -radius; dy <= radius; dy++) { + for (int dz = -radius; dz <= radius; dz++) { + if (dx * dx + dy * dy + dz * dz <= radius * radius) { + BlockPos pos = new BlockPos(centerX + dx, centerY + dy, centerZ + dz); + BlockState state = world.getBlockState(pos); + if (!state.isOf(Blocks.BLACK_CONCRETE)) { + NbtCompound nbt = null; + BlockEntity be = world.getBlockEntity(pos); + if (be != null) { + nbt = be.createNbtWithIdentifyingData(world.getRegistryManager()); + } + boolean isFirst = GlobalBlockRegistry.acquire(world, pos, state, nbt); + if (isFirst && be != null) { + if (be instanceof Inventory) { + ((Inventory) be).clear(); + } + world.removeBlockEntity(pos); + } + world.setBlockState(pos, Blocks.BLACK_CONCRETE.getDefaultState(), BUILD_FLAGS); + } + blockSet.add(pos); + allBuiltPositions.add(pos); + } + } + } + } + } + + private void clearSphereBlocks(Set blocks) { + for (BlockPos pos : blocks) { + boolean shouldRestore = GlobalBlockRegistry.release(world, pos); + if (shouldRestore) { + BlockState original = GlobalBlockRegistry.getOriginalState(world, pos); + if (original != null) { + world.setBlockState(pos, original, RESTORE_FLAGS); + NbtCompound nbt = GlobalBlockRegistry.getOriginalNbt(world, pos); + if (nbt != null) { + BlockEntity newBe = BlockEntity.createFromNbt(pos, original, nbt, world.getRegistryManager()); + if (newBe != null) world.addBlockEntity(newBe); + } + } else { + world.setBlockState(pos, Blocks.AIR.getDefaultState(), RESTORE_FLAGS); + } + GlobalBlockRegistry.clearEntry(world, pos); + } + } + } + + private void clearAllSphereBlocks() { + if (!lastCenterSphereBlocks.isEmpty()) { + clearSphereBlocks(lastCenterSphereBlocks); + lastCenterSphereBlocks.clear(); + } + for (int i = 0; i < 6; i++) { + if (!lastSmallSphereBlocks.get(i).isEmpty()) { + clearSphereBlocks(lastSmallSphereBlocks.get(i)); + lastSmallSphereBlocks.get(i).clear(); + } + } + blocksCleared = true; + } + + private void spawnBlackSphereParticles() { + world.spawnParticles(ParticleTypes.SQUID_INK, false, false, cx, sphereCenterY, cz, 20, maxSphereRadius, maxSphereRadius, maxSphereRadius, 0.1); + world.spawnParticles(ParticleTypes.ASH, false, false, cx, sphereCenterY, cz, 15, maxSphereRadius, maxSphereRadius, maxSphereRadius, 0.05); + } + + private void spawnBlackDripParticles(double fromX, double fromY, double fromZ, double toX, double toY, double toZ) { + int steps = 10; + for (int i = 0; i < steps; i++) { + double t = (double) i / steps; + double px = fromX + (toX - fromX) * t; + double py = fromY + (toY - fromY) * t; + double pz = fromZ + (toZ - fromZ) * t; + world.spawnParticles(ParticleTypes.SQUID_INK, false, false, px, py, pz, 3, 0.1, 0.1, 0.1, 0.0); + world.spawnParticles(ParticleTypes.ASH, false, false, px, py, pz, 2, 0.1, 0.1, 0.1, 0.0); + } + } + + private void spawnRangeBorder() { + double borderRadius = (double) range + 0.5; + int segments = Math.max(32, range * 8); + for (int i = 0; i < segments; i++) { + double angle = (Math.PI * 2 * i) / segments; + double rx = cx + Math.cos(angle) * borderRadius; + double rz = cz + Math.sin(angle) * borderRadius; + world.spawnParticles(ParticleTypes.FLAME, false, false, rx, topY + 1.0, rz, 3, 0.02, 0.0, 0.02, 0.0); + world.spawnParticles(ParticleTypes.CRIT, false, false, rx, topY + 0.2, rz, 2, 0.02, 0.0, 0.02, 0.0); + } + playSoundToAll(cx, topY, cz, SoundEvents.BLOCK_AMETHYST_BLOCK_CHIME, SoundCategory.BLOCKS, 1.5f, 0.8f); + } + + private void summonRandomLightningInArea() { + double angle = Math.random() * Math.PI * 2; + double r = Math.random() * range; + double lx = cx + Math.cos(angle) * r; + double lz = cz + Math.sin(angle) * r; + int ly = world.getTopY(Heightmap.Type.WORLD_SURFACE, (int) lx, (int) lz); + LightningEntity lightning = EntityType.LIGHTNING_BOLT.create(world, SpawnReason.COMMAND); + if (lightning != null) { + lightning.refreshPositionAfterTeleport(lx, ly, lz); + world.spawnEntity(lightning); + } + } + + private void spawnRandomSmallExplosion() { + double angle = Math.random() * Math.PI * 2; + double r = Math.random() * range; + double ex = cx + Math.cos(angle) * r; + double ez = cz + Math.sin(angle) * r; + int ey = world.getTopY(Heightmap.Type.WORLD_SURFACE, (int) ex, (int) ez); + world.createExplosion(null, ex, ey, ez, 2.0f, false, World.ExplosionSourceType.TNT); + world.spawnParticles(ParticleTypes.FLAME, false, false, ex, ey + 1.0, ez, 20, 0.5, 0.5, 0.5, 0.2); + playSoundToAll(ex, ey, ez, SoundEvents.ENTITY_FIREWORK_ROCKET_BLAST, SoundCategory.AMBIENT, 1.5f, 0.6f); + } + + private void spawnBurstParticles() { + for (int i = 0; i < 24; i++) { + double angle = Math.random() * Math.PI * 2; + double radius = Math.random() * 6.0; + double rx = cx + Math.cos(angle) * radius; + double rz = cz + Math.sin(angle) * radius; + world.spawnParticles(ParticleTypes.FLAME, false, false, rx, topY + Math.random() * 4, rz, 6, 0.1, 0.1, 0.1, 0.05); + world.spawnParticles(ParticleTypes.CRIT, false, false, rx, topY + Math.random() * 4, rz, 6, 0.1, 0.1, 0.1, 0.0); + } + world.spawnParticles(ParticleTypes.TOTEM_OF_UNDYING, false, false, cx, topY + 1.0, cz, 60, 2.5, 2.5, 2.5, 0.3); + world.spawnParticles(ParticleTypes.END_ROD, false, false, cx, topY + 1.0, cz, 30, 1.5, 1.5, 1.5, 0.2); + world.spawnParticles(ParticleTypes.CAMPFIRE_COSY_SMOKE, false, false, cx, topY + 1.0, cz, 20, 1.0, 1.0, 1.0, 0.05); + world.spawnParticles(ParticleTypes.ENCHANT, false, false, cx, topY + 1.0, cz, 30, 2.0, 2.0, 2.0, 0.3); + } + + private void executeFinalExplosion() { + int centerTopY = (int) topY; + int upwardHeight = Math.max(5, range / 2); + int bottomY = (depth == -1) ? world.getBottomY() + 1 : (centerTopY - depth); + double rangeSq = (double) range * (double) range; + + for (int bx = x - range; bx <= x + range; bx++) { + for (int bz = z - range; bz <= z + range; bz++) { + int dx = bx - x; + int dz = bz - z; + if ((double) (dx * dx + dz * dz) <= rangeSq) { + int columnTopY = world.getTopY(Heightmap.Type.WORLD_SURFACE, bx, bz) + upwardHeight; + for (int by = columnTopY; by >= bottomY; by--) { + world.removeBlock(new BlockPos(bx, by, bz), false); + } + } + } + } + + // 清理受灾区域的液体(液体方块和流动液体) + for (int bx = x - range; bx <= x + range; bx++) { + for (int bz = z - range; bz <= z + range; bz++) { + int dx = bx - x; + int dz = bz - z; + if ((double) (dx * dx + dz * dz) <= rangeSq) { + int columnTopY = world.getTopY(Heightmap.Type.WORLD_SURFACE, bx, bz) + upwardHeight; + for (int by = columnTopY; by >= bottomY; by--) { + BlockPos pos = new BlockPos(bx, by, bz); + BlockState state = world.getBlockState(pos); + if (!state.getFluidState().isEmpty()) { + world.setBlockState(pos, Blocks.AIR.getDefaultState()); + } + } + } + } + } + + world.createExplosion(null, cx, topY, cz, 8.0f, true, World.ExplosionSourceType.TNT); + + restoreRecordedBlocks(); + + playSoundToAll(cx, topY, cz, SoundEvents.ENTITY_ENDER_DRAGON_GROWL, SoundCategory.HOSTILE, 2.0f, 0.5f); + playSoundToAll(cx, topY, cz, SoundEvents.ENTITY_WITHER_SPAWN, SoundCategory.HOSTILE, 2.0f, 0.6f); + playSoundToAll(cx, topY, cz, SoundEvents.ITEM_TOTEM_USE, SoundCategory.PLAYERS, 2.0f, 0.5f); + playSoundToAll(cx, topY, cz, SoundEvents.BLOCK_BELL_USE, SoundCategory.BLOCKS, 2.0f, 0.5f); + playSoundToAll(cx, topY, cz, SoundEvents.ENTITY_FIREWORK_ROCKET_BLAST, SoundCategory.AMBIENT, 2.0f, 0.5f); + playSoundToAll(cx, topY, cz, SoundEvents.UI_TOAST_CHALLENGE_COMPLETE, SoundCategory.MASTER, 2.0f, 0.5f); + playSoundToAll(cx, topY, cz, SoundEvents.ENTITY_LIGHTNING_BOLT_THUNDER, SoundCategory.WEATHER, 2.0f, 0.5f); + playSoundToAll(cx, topY, cz, SoundEvents.BLOCK_PORTAL_TRIGGER, SoundCategory.AMBIENT, 2.0f, 0.5f); + } + + private void restoreRecordedBlocks() { + restoreRemainingBlocks(true); + } + + private void restoreAllRecordedBlocks() { + restoreRemainingBlocks(false); + } + } + + private static class MeteoSession implements TestSession { + private static final int EXPLOSION_RADIUS = 20; + private static final int METEO_FALL_TICKS = 200; + private static final int POST_EXPLOSION_DURATION = 400; + + private final int id = NextTestId.getAndIncrement(); + private final ServerWorld world; + private final int targetX, targetY, targetZ; + private int tickCounter = 0; + private boolean finished = false; + private final ServerBossBar bossBar; + private boolean explosionExecuted = false; + private int explosionExecutedTick = -1; + + private final List meteos = new ArrayList<>(); + private final Set allBuiltPositions = new HashSet<>(); + private static final int SKIP_DROPS_FLAGS = Block.SKIP_DROPS | Block.MOVED; + private static final int RESTORE_FLAGS = Block.SKIP_DROPS | Block.NOTIFY_LISTENERS; + + private static class Meteo { + double startX, startY, startZ; + double currentX, currentY, currentZ; + double targetX, targetY, targetZ; + int radius; + Set currentBlocks = new HashSet<>(); + boolean arrived = false; + boolean isMain; + + Meteo(double sx, double sy, double sz, double tx, double ty, double tz, int r, boolean main) { + startX = sx; startY = sy; startZ = sz; + currentX = sx; currentY = sy; currentZ = sz; + targetX = tx; targetY = ty; targetZ = tz; + radius = r; + isMain = main; + } + } + + MeteoSession(ServerWorld world, int x, int y, int z) { + this.world = world; + this.targetX = x; + this.targetY = y; + this.targetZ = z; + + int worldHeight = world.getHeight(); + int startY = Math.min(worldHeight, targetY + 80 + (int)(Math.random() * 40)); + + double mainAngle = Math.random() * Math.PI * 2; + double mainDistance = 70 + Math.random() * 30; + double mainSx = x + 0.5 + Math.cos(mainAngle) * mainDistance; + double mainSz = z + 0.5 + Math.sin(mainAngle) * mainDistance; + meteos.add(new Meteo(mainSx, startY, mainSz, x + 0.5, y, z + 0.5, 4, true)); + + this.bossBar = new ServerBossBar( + Text.literal("§6§l[METEO] §e陨石来袭!"), + BossBar.Color.RED, + BossBar.Style.NOTCHED_20 + ); + this.bossBar.setVisible(true); + this.bossBar.setPercent(1.0f); + var players = world.getServer().getPlayerManager().getPlayerList(); + for (ServerPlayerEntity player : players) { + this.bossBar.addPlayer(player); + } + } + + boolean isFinished() { + return finished; + } + + @Override + public int getId() { + return id; + } + + @Override + public String getType() { + return "METEO"; + } + + @Override + public String getDescription() { + String worldName = world.getRegistryKey().getValue().toString(); + String phase = explosionExecuted ? "爆炸后" : (tickCounter < METEO_FALL_TICKS ? "陨石坠落" : "即将爆炸"); + return String.format("§6[METEO#%d] §7世界: %s §7| 目标: %d, %d, %d §7| 阶段: %s §7| tick: %d/%d", + id, worldName, targetX, targetY, targetZ, phase, tickCounter, explosionExecuted ? explosionExecutedTick + POST_EXPLOSION_DURATION : METEO_FALL_TICKS); + } + + @Override + public void cancel() { + if (finished) return; + for (Meteo meteo : meteos) { + if (!meteo.currentBlocks.isEmpty()) { + clearMeteoBlocks(meteo.currentBlocks); + meteo.currentBlocks.clear(); + } + } + for (BlockPos pos : allBuiltPositions) { + boolean shouldRestore = GlobalBlockRegistry.release(world, pos); + if (shouldRestore) { + BlockState original = GlobalBlockRegistry.getOriginalState(world, pos); + if (original != null) { + world.setBlockState(pos, original, RESTORE_FLAGS); + NbtCompound nbt = GlobalBlockRegistry.getOriginalNbt(world, pos); + if (nbt != null) { + BlockEntity newBe = BlockEntity.createFromNbt(pos, original, nbt, world.getRegistryManager()); + if (newBe != null) world.addBlockEntity(newBe); + } + } + GlobalBlockRegistry.clearEntry(world, pos); + } + } + allBuiltPositions.clear(); + bossBar.clearPlayers(); + bossBar.setVisible(false); + finished = true; + } + + void tick() { + if (finished) return; + + if (explosionExecuted) { + if (tickCounter >= explosionExecutedTick + POST_EXPLOSION_DURATION) { + bossBar.clearPlayers(); + bossBar.setVisible(false); + finished = true; + } else { + updateBossBarPostExplosion(); + } + tickCounter++; + return; + } + + updateBossBar(); + + double progress = Math.min(1.0, (double) tickCounter / METEO_FALL_TICKS); + + for (Meteo meteo : meteos) { + if (meteo.arrived) continue; + + if (!meteo.currentBlocks.isEmpty()) { + clearMeteoBlocks(meteo.currentBlocks); + meteo.currentBlocks.clear(); + } + + meteo.currentX = meteo.startX + (meteo.targetX - meteo.startX) * progress; + meteo.currentY = meteo.startY + (meteo.targetY - meteo.startY) * progress; + meteo.currentZ = meteo.startZ + (meteo.targetZ - meteo.startZ) * progress; + + buildMeteo(meteo); + spawnMeteoParticles(meteo); + + if (progress >= 1.0) { + meteo.arrived = true; + } + } + + if (tickCounter % 40 == 0) { + playSoundToAll(targetX + 0.5, targetY, targetZ + 0.5, SoundEvents.ENTITY_FIREWORK_ROCKET_LAUNCH, SoundCategory.AMBIENT, 1.0f, 0.5f); + } + + if (meteos.get(0).arrived) { + executeExplosion(); + explosionExecuted = true; + explosionExecutedTick = tickCounter; + } + + tickCounter++; + } + + private void buildMeteo(Meteo meteo) { + int cx = (int) Math.round(meteo.currentX); + int cy = (int) Math.round(meteo.currentY); + int cz = (int) Math.round(meteo.currentZ); + int r = meteo.radius; + + for (int dx = -r; dx <= r; dx++) { + for (int dy = -r; dy <= r; dy++) { + for (int dz = -r; dz <= r; dz++) { + int distSq = dx * dx + dy * dy + dz * dz; + if (distSq <= r * r) { + BlockPos pos = new BlockPos(cx + dx, cy + dy, cz + dz); + BlockState state = world.getBlockState(pos); + if (!isMeteoBlock(state) && !state.isOf(Blocks.BLACK_CONCRETE)) { + NbtCompound nbt = null; + BlockEntity be = world.getBlockEntity(pos); + if (be != null) { + nbt = be.createNbtWithIdentifyingData(world.getRegistryManager()); + } + boolean isFirst = GlobalBlockRegistry.acquire(world, pos, state, nbt); + if (isFirst && be != null) { + if (be instanceof Inventory) { + ((Inventory) be).clear(); + } + world.removeBlockEntity(pos); + } + world.setBlockState(pos, chooseMeteoBlock(distSq, r), SKIP_DROPS_FLAGS); + } + meteo.currentBlocks.add(pos); + allBuiltPositions.add(pos); + } + } + } + } + } + + private boolean isMeteoBlock(BlockState state) { + return state.isOf(Blocks.MAGMA_BLOCK) || state.isOf(Blocks.COBBLESTONE) || state.isOf(Blocks.STONE); + } + + private BlockState chooseMeteoBlock(int distSq, int r) { + double rand = Math.random(); + if (distSq >= (r - 1) * (r - 1)) { + // 外壳:更多岩浆块 + if (rand < 0.5) return Blocks.MAGMA_BLOCK.getDefaultState(); + if (rand < 0.75) return Blocks.COBBLESTONE.getDefaultState(); + return Blocks.STONE.getDefaultState(); + } else { + // 内核:更多石头和圆石 + if (rand < 0.4) return Blocks.STONE.getDefaultState(); + if (rand < 0.75) return Blocks.COBBLESTONE.getDefaultState(); + return Blocks.MAGMA_BLOCK.getDefaultState(); + } + } + + private void clearMeteoBlocks(Set blocks) { + for (BlockPos pos : blocks) { + BlockState current = world.getBlockState(pos); + if (isMeteoBlock(current)) { + boolean shouldRestore = GlobalBlockRegistry.release(world, pos); + if (shouldRestore) { + BlockState original = GlobalBlockRegistry.getOriginalState(world, pos); + if (original != null) { + world.setBlockState(pos, original, RESTORE_FLAGS); + NbtCompound nbt = GlobalBlockRegistry.getOriginalNbt(world, pos); + if (nbt != null) { + BlockEntity newBe = BlockEntity.createFromNbt(pos, original, nbt, world.getRegistryManager()); + if (newBe != null) world.addBlockEntity(newBe); + } + } else { + world.removeBlock(pos, false); + } + GlobalBlockRegistry.clearEntry(world, pos); + } + } + } + } + + private void spawnMeteoParticles(Meteo meteo) { + world.spawnParticles(ParticleTypes.FLAME, false, false, meteo.currentX, meteo.currentY, meteo.currentZ, 15, 0.5, 0.5, 0.5, 0.1); + world.spawnParticles(ParticleTypes.LAVA, false, false, meteo.currentX, meteo.currentY, meteo.currentZ, 5, 0.3, 0.3, 0.3, 0.1); + world.spawnParticles(ParticleTypes.CAMPFIRE_COSY_SMOKE, false, false, meteo.currentX, meteo.currentY + 1, meteo.currentZ, 10, 0.5, 0.5, 0.5, 0.05); + } + + private void executeExplosion() { + for (Meteo meteo : meteos) { + if (!meteo.currentBlocks.isEmpty()) { + clearMeteoBlocks(meteo.currentBlocks); + meteo.currentBlocks.clear(); + } + } + + int r = EXPLOSION_RADIUS; + for (int dx = -r; dx <= r; dx++) { + for (int dy = -r; dy <= r; dy++) { + for (int dz = -r; dz <= r; dz++) { + if (dx * dx + dy * dy + dz * dz <= r * r) { + BlockPos pos = new BlockPos(targetX + dx, targetY + dy, targetZ + dz); + world.removeBlock(pos, false); + } + } + } + } + + // 清理液体(液体方块和流动液体) + for (int dx = -r; dx <= r; dx++) { + for (int dy = -r; dy <= r; dy++) { + for (int dz = -r; dz <= r; dz++) { + if (dx * dx + dy * dy + dz * dz <= r * r) { + BlockPos pos = new BlockPos(targetX + dx, targetY + dy, targetZ + dz); + BlockState state = world.getBlockState(pos); + if (!state.getFluidState().isEmpty()) { + world.setBlockState(pos, Blocks.AIR.getDefaultState()); + } + } + } + } + } + + generateOres(); + placeMeteoRemains(); + placeChest(); + + world.createExplosion(null, targetX + 0.5, targetY, targetZ + 0.5, 8.0f, true, World.ExplosionSourceType.TNT); + + playSoundToAll(targetX + 0.5, targetY, targetZ + 0.5, SoundEvents.ENTITY_GENERIC_EXPLODE.value(), SoundCategory.BLOCKS, 3.0f, 0.6f); + playSoundToAll(targetX + 0.5, targetY, targetZ + 0.5, SoundEvents.ENTITY_ENDER_DRAGON_GROWL, SoundCategory.HOSTILE, 2.0f, 0.5f); + playSoundToAll(targetX + 0.5, targetY, targetZ + 0.5, SoundEvents.ENTITY_WITHER_SPAWN, SoundCategory.HOSTILE, 2.0f, 0.6f); + playSoundToAll(targetX + 0.5, targetY, targetZ + 0.5, SoundEvents.ENTITY_FIREWORK_ROCKET_BLAST, SoundCategory.AMBIENT, 2.0f, 0.5f); + playSoundToAll(targetX + 0.5, targetY, targetZ + 0.5, SoundEvents.ENTITY_LIGHTNING_BOLT_THUNDER, SoundCategory.WEATHER, 2.0f, 0.5f); + } + + private void generateOres() { + int r = EXPLOSION_RADIUS; + BlockState[] ores = { + Blocks.DIAMOND_ORE.getDefaultState(), + Blocks.GOLD_ORE.getDefaultState(), + Blocks.IRON_ORE.getDefaultState(), + Blocks.EMERALD_ORE.getDefaultState(), + Blocks.REDSTONE_ORE.getDefaultState(), + Blocks.COAL_ORE.getDefaultState(), + Blocks.LAPIS_ORE.getDefaultState(), + Blocks.NETHER_QUARTZ_ORE.getDefaultState(), + }; + + for (int dx = -r; dx <= r; dx++) { + for (int dy = -r; dy <= r; dy++) { + for (int dz = -r; dz <= r; dz++) { + int distSq = dx * dx + dy * dy + dz * dz; + if (distSq <= r * r && distSq >= (r - 1) * (r - 1)) { + BlockPos pos = new BlockPos(targetX + dx, targetY + dy, targetZ + dz); + BlockState current = world.getBlockState(pos); + if (current.isAir() && hasSolidNeighbor(pos) && Math.random() < 0.08) { + BlockState ore = ores[(int) (Math.random() * ores.length)]; + world.setBlockState(pos, ore); + } + } + } + } + } + } + + private boolean hasSolidNeighbor(BlockPos pos) { + BlockPos[] neighbors = { + pos.up(), pos.down(), pos.north(), pos.south(), pos.east(), pos.west() + }; + for (BlockPos neighbor : neighbors) { + BlockState state = world.getBlockState(neighbor); + if (!state.isAir() && state.getFluidState().isEmpty()) { + return true; + } + } + return false; + } + + private void placeMeteoRemains() { + int bottomY = targetY - EXPLOSION_RADIUS + 2; + int r = 3; + for (int dx = -r; dx <= r; dx++) { + for (int dy = -r; dy <= r; dy++) { + for (int dz = -r; dz <= r; dz++) { + int distSq = dx * dx + dy * dy + dz * dz; + if (distSq <= r * r) { + BlockPos pos = new BlockPos(targetX + dx, bottomY + dy, targetZ + dz); + world.setBlockState(pos, chooseMeteoBlock(distSq, r)); + } + } + } + } + } + + private void placeChest() { + int bottomY = targetY - EXPLOSION_RADIUS + 2; + BlockPos chestPos = new BlockPos(targetX, bottomY + 4, targetZ); + world.setBlockState(chestPos, Blocks.CHEST.getDefaultState()); + + var blockEntity = world.getBlockEntity(chestPos); + if (blockEntity instanceof net.minecraft.block.entity.ChestBlockEntity) { + var chest = (net.minecraft.block.entity.ChestBlockEntity) blockEntity; + chest.setStack(0, new net.minecraft.item.ItemStack(net.minecraft.item.Items.DIAMOND, 3 + (int)(Math.random() * 5))); + chest.setStack(1, new net.minecraft.item.ItemStack(net.minecraft.item.Items.GOLD_INGOT, 5 + (int)(Math.random() * 10))); + chest.setStack(2, new net.minecraft.item.ItemStack(net.minecraft.item.Items.IRON_INGOT, 10 + (int)(Math.random() * 20))); + chest.setStack(3, new net.minecraft.item.ItemStack(net.minecraft.item.Items.EMERALD, 2 + (int)(Math.random() * 4))); + chest.setStack(4, new net.minecraft.item.ItemStack(net.minecraft.item.Items.EXPERIENCE_BOTTLE, 5 + (int)(Math.random() * 10))); + chest.setStack(5, new net.minecraft.item.ItemStack(net.minecraft.item.Items.GOLDEN_APPLE, 2 + (int)(Math.random() * 3))); + chest.setStack(6, new net.minecraft.item.ItemStack(net.minecraft.item.Items.NETHERITE_SCRAP, 1 + (int)(Math.random() * 2))); + chest.setStack(7, new net.minecraft.item.ItemStack(net.minecraft.item.Items.ENDER_PEARL, 3 + (int)(Math.random() * 5))); + } + } + + private void playSoundToAll(double x, double y, double z, net.minecraft.sound.SoundEvent sound, net.minecraft.sound.SoundCategory category, float volume, float pitch) { + world.playSound(null, x, y, z, sound, category, 10000.0f, pitch); + } + + private void updateBossBar() { + int remainingTicks = Math.max(0, METEO_FALL_TICKS - tickCounter); + int remainingSeconds = remainingTicks / 20; + + Text title = Text.literal(String.format("§6§l[METEO] §e目标: %d, %d, %d §7| §c陨石来袭 §7| §e倒计时: %ds", targetX, targetY, targetZ, remainingSeconds)); + this.bossBar.setName(title); + float progress = (float) tickCounter / METEO_FALL_TICKS; + this.bossBar.setPercent(Math.max(0.0f, Math.min(1.0f, progress))); + this.bossBar.setColor(BossBar.Color.RED); + + refreshBossBarPlayers(); + } + + private void updateBossBarPostExplosion() { + int elapsed = tickCounter - explosionExecutedTick; + int remainingTicks = Math.max(0, POST_EXPLOSION_DURATION - elapsed); + int remainingSeconds = remainingTicks / 20; + + Text title = Text.literal(String.format("§a§l[METEO] §e陨石已坠落!§7| §eHUD消失: %ds", remainingSeconds)); + this.bossBar.setName(title); + float progress = (float) elapsed / POST_EXPLOSION_DURATION; + this.bossBar.setPercent(Math.max(0.0f, 1.0f - progress)); + this.bossBar.setColor(BossBar.Color.GREEN); + + refreshBossBarPlayers(); + } + + private void refreshBossBarPlayers() { + if (tickCounter % 10 == 0) { + var playerList = world.getServer().getPlayerManager().getPlayerList(); + var existingPlayers = bossBar.getPlayers(); + java.util.Set existingUUIDs = new java.util.HashSet<>(); + for (ServerPlayerEntity p : existingPlayers) { + existingUUIDs.add(p.getUuid()); + } + for (ServerPlayerEntity player : playerList) { + if (!existingUUIDs.contains(player.getUuid())) { + bossBar.addPlayer(player); + } + } + } + } + } + + private static class LineSession implements TestSession { + private static final int TOTAL_TICKS = 1200; + private static final int WAVE_INTERVAL_TICKS = 87; + private static final double WAVE_EXPAND_SPEED = 0.15; + private static final int BLOCK_RESTORE_DELAY_TICKS = 13; + private static final int BUILD_FLAGS = Block.SKIP_DROPS | Block.MOVED | Block.NOTIFY_LISTENERS; + private static final int RESTORE_FLAGS = Block.SKIP_DROPS | Block.NOTIFY_LISTENERS; + + private final int id = NextTestId.getAndIncrement(); + private final ServerWorld world; + private final int x; + private final int z; + private final double cx; + private final double cz; + private final int topY; + private final int range; + private int tickCounter = 0; + private boolean finished = false; + private final ServerBossBar bossBar; + + private static class Wave { + double currentRadius; + boolean active; + Wave(double startRadius) { + this.currentRadius = startRadius; + this.active = true; + } + } + + private static class PendingRestore { + BlockPos pos; + int restoreAtTick; + + PendingRestore(BlockPos pos, int restoreAtTick) { + this.pos = pos; + this.restoreAtTick = restoreAtTick; + } + } + + private final List waves = new ArrayList<>(); + private final List pendingRestores = new ArrayList<>(); + private final Set allBuiltPositions = new HashSet<>(); + + LineSession(ServerWorld world, int x, int z, int topY, int range) { + this.world = world; + this.x = x; + this.z = z; + this.cx = x + 0.5; + this.cz = z + 0.5; + this.topY = topY; + this.range = range; + this.bossBar = new ServerBossBar( + Text.literal("§b§l[LINE] §e初始化中..."), + BossBar.Color.BLUE, + BossBar.Style.NOTCHED_20 + ); + this.bossBar.setVisible(true); + this.bossBar.setPercent(1.0f); + var players = world.getServer().getPlayerManager().getPlayerList(); + for (ServerPlayerEntity player : players) { + this.bossBar.addPlayer(player); + } + spawnInitialBurst(); + } + + boolean isFinished() { + return finished; + } + + @Override + public int getId() { + return id; + } + + @Override + public String getType() { + return "LINE"; + } + + @Override + public String getDescription() { + String worldName = world.getRegistryKey().getValue().toString(); + return String.format("§b[LINE#%d] §7世界: %s §7| 中心: %d, %d, %d §7| 范围: %d §7| 波纹: %d §7| 黑块: %d §7| tick: %d/%d", + id, worldName, x, topY, z, range, waves.size(), allBuiltPositions.size(), tickCounter, TOTAL_TICKS); + } + + @Override + public void cancel() { + if (finished) return; + pendingRestores.clear(); + waves.clear(); + for (BlockPos pos : allBuiltPositions) { + boolean shouldRestore = GlobalBlockRegistry.release(world, pos); + if (shouldRestore) { + BlockState original = GlobalBlockRegistry.getOriginalState(world, pos); + if (original != null && world.getBlockState(pos).isOf(Blocks.BLACK_CONCRETE)) { + world.setBlockState(pos, original, RESTORE_FLAGS); + NbtCompound nbt = GlobalBlockRegistry.getOriginalNbt(world, pos); + if (nbt != null) { + BlockEntity newBe = BlockEntity.createFromNbt(pos, original, nbt, world.getRegistryManager()); + if (newBe != null) world.addBlockEntity(newBe); + } + } + GlobalBlockRegistry.clearEntry(world, pos); + } + } + allBuiltPositions.clear(); + bossBar.clearPlayers(); + bossBar.setVisible(false); + finished = true; + } + + private void playSoundToAll(double sx, double sy, double sz, net.minecraft.sound.SoundEvent sound, net.minecraft.sound.SoundCategory category, float volume, float pitch) { + world.playSound(null, sx, sy, sz, sound, category, 10000.0f, pitch); + } + + private void spawnInitialBurst() { + world.spawnParticles(ParticleTypes.END_ROD, false, false, cx, topY + 1.0, cz, 40, 0.5, 1.0, 0.5, 0.1); + world.spawnParticles(ParticleTypes.NAUTILUS, false, false, cx, topY + 1.0, cz, 30, 0.5, 1.0, 0.5, 0.1); + playSoundToAll(cx, topY + 1.0, cz, SoundEvents.BLOCK_CONDUIT_ACTIVATE, SoundCategory.BLOCKS, 1.5f, 1.2f); + playSoundToAll(cx, topY + 1.0, cz, SoundEvents.BLOCK_BEACON_ACTIVATE, SoundCategory.BLOCKS, 1.5f, 1.5f); + } + + private void spawnNewWave() { + waves.add(new Wave(0.5)); + playSoundToAll(cx, topY + 1.0, cz, SoundEvents.BLOCK_CONDUIT_AMBIENT_SHORT, SoundCategory.BLOCKS, 1.2f, 1.0f + 0.1f * waves.size()); + playSoundToAll(cx, topY + 1.0, cz, SoundEvents.BLOCK_BELL_USE, SoundCategory.BLOCKS, 1.0f, 1.5f); + } + + void tick() { + if (finished) return; + tickCounter++; + + if (tickCounter <= TOTAL_TICKS && tickCounter % WAVE_INTERVAL_TICKS == 0) { + spawnNewWave(); + } + + Iterator iterator = waves.iterator(); + while (iterator.hasNext()) { + Wave wave = iterator.next(); + if (!wave.active) { + iterator.remove(); + continue; + } + wave.currentRadius += WAVE_EXPAND_SPEED; + if (wave.currentRadius > range) { + wave.active = false; + iterator.remove(); + continue; + } + placeRingBlocks(wave.currentRadius); + } + + processPendingRestores(); + + if (tickCounter >= TOTAL_TICKS && waves.isEmpty() && pendingRestores.isEmpty()) { + forceRestoreAll(); + finish(); + } + + updateBossBar(); + } + + private void placeRingBlocks(double ringRadius) { + double ringWidth = 1.0; + double innerRadius = ringRadius; + double outerRadius = ringRadius + ringWidth; + double innerSq = innerRadius * innerRadius; + double outerSq = outerRadius * outerRadius; + + int minX = x - (int) Math.ceil(outerRadius); + int maxX = x + (int) Math.ceil(outerRadius); + int minZ = z - (int) Math.ceil(outerRadius); + int maxZ = z + (int) Math.ceil(outerRadius); + + for (int bx = minX; bx <= maxX; bx++) { + for (int bz = minZ; bz <= maxZ; bz++) { + int dx = bx - x; + int dz = bz - z; + double distSq = dx * dx + dz * dz; + if (distSq >= innerSq && distSq <= outerSq) { + int topYPlusOne = world.getTopY(Heightmap.Type.WORLD_SURFACE, bx, bz); + int blockTopY = topYPlusOne - 1; + if (blockTopY < world.getBottomY()) continue; + BlockPos pos = new BlockPos(bx, blockTopY, bz); + + if (allBuiltPositions.contains(pos)) continue; + + BlockState currentState = world.getBlockState(pos); + if (currentState.isOf(Blocks.BLACK_CONCRETE)) continue; + if (currentState.isAir()) continue; + if (!currentState.getFluidState().isEmpty()) continue; + + NbtCompound nbt = null; + BlockEntity be = world.getBlockEntity(pos); + if (be != null) { + nbt = be.createNbtWithIdentifyingData(world.getRegistryManager()); + } + boolean isFirst = GlobalBlockRegistry.acquire(world, pos, currentState, nbt); + if (isFirst && be != null) { + if (be instanceof Inventory) { + ((Inventory) be).clear(); + } + world.removeBlockEntity(pos); + } + + world.setBlockState(pos, Blocks.BLACK_CONCRETE.getDefaultState(), BUILD_FLAGS); + allBuiltPositions.add(pos); + + pendingRestores.add(new PendingRestore(pos, tickCounter + BLOCK_RESTORE_DELAY_TICKS)); + + double px = bx + 0.5; + double pz = bz + 0.5; + double py = blockTopY + 1.0; + world.spawnParticles(ParticleTypes.END_ROD, false, false, px, py, pz, 2, 0.1, 0.3, 0.1, 0.03); + } + } + } + } + + private void processPendingRestores() { + Iterator it = pendingRestores.iterator(); + while (it.hasNext()) { + PendingRestore pr = it.next(); + if (tickCounter >= pr.restoreAtTick) { + restoreBlock(pr); + it.remove(); + } + } + } + + private void restoreBlock(PendingRestore pr) { + BlockPos pos = pr.pos; + if (!world.getBlockState(pos).isOf(Blocks.BLACK_CONCRETE)) { + boolean shouldClear = GlobalBlockRegistry.release(world, pos); + if (shouldClear) GlobalBlockRegistry.clearEntry(world, pos); + allBuiltPositions.remove(pos); + return; + } + boolean shouldRestore = GlobalBlockRegistry.release(world, pos); + if (shouldRestore) { + BlockState original = GlobalBlockRegistry.getOriginalState(world, pos); + if (original != null) { + world.setBlockState(pos, original, RESTORE_FLAGS); + NbtCompound nbt = GlobalBlockRegistry.getOriginalNbt(world, pos); + if (nbt != null) { + BlockEntity newBe = BlockEntity.createFromNbt(pos, original, nbt, world.getRegistryManager()); + if (newBe != null) world.addBlockEntity(newBe); + } + } + GlobalBlockRegistry.clearEntry(world, pos); + } + allBuiltPositions.remove(pos); + + double px = pos.getX() + 0.5; + double py = pos.getY() + 1.0; + double pz = pos.getZ() + 0.5; + world.spawnParticles(ParticleTypes.END_ROD, false, false, px, py, pz, 2, 0.05, 0.1, 0.05, 0.02); + } + + private void forceRestoreAll() { + for (PendingRestore pr : pendingRestores) { + restoreBlock(pr); + } + pendingRestores.clear(); + + for (BlockPos pos : new HashSet<>(allBuiltPositions)) { + if (world.getBlockState(pos).isOf(Blocks.BLACK_CONCRETE)) { + boolean shouldRestore = GlobalBlockRegistry.release(world, pos); + if (shouldRestore) { + BlockState original = GlobalBlockRegistry.getOriginalState(world, pos); + if (original != null) { + world.setBlockState(pos, original, RESTORE_FLAGS); + NbtCompound nbt = GlobalBlockRegistry.getOriginalNbt(world, pos); + if (nbt != null) { + BlockEntity newBe = BlockEntity.createFromNbt(pos, original, nbt, world.getRegistryManager()); + if (newBe != null) world.addBlockEntity(newBe); + } + } + GlobalBlockRegistry.clearEntry(world, pos); + } + } + } + allBuiltPositions.clear(); + } + + private void updateBossBar() { + int elapsed = tickCounter; + int remainingTicks = Math.max(0, TOTAL_TICKS - elapsed); + int activeWaves = waves.size(); + int activeBlocks = allBuiltPositions.size(); + + int totalSeconds = remainingTicks / 20; + int minutes = totalSeconds / 60; + int seconds = totalSeconds % 60; + String timeStr = minutes > 0 ? String.format("%dm%02ds", minutes, seconds) : String.format("%ds", seconds); + + Text title = Text.literal(String.format("§b§l[LINE] §e中心: %d, %d, %d §7| §e范围: %d §7| §b波纹: %d §7| §8黑块: %d §7| §e剩余: %s", + x, topY, z, range, activeWaves, activeBlocks, timeStr)); + bossBar.setName(title); + + float progress; + if (tickCounter >= TOTAL_TICKS) { + progress = (waves.isEmpty() && pendingRestores.isEmpty()) ? 1.0f : 0.95f; + } else { + progress = (float) elapsed / TOTAL_TICKS; + } + bossBar.setPercent(Math.max(0.0f, Math.min(1.0f, progress))); + + if (tickCounter % 10 == 0) { + var playerList = world.getServer().getPlayerManager().getPlayerList(); + var existingPlayers = bossBar.getPlayers(); + java.util.Set existingUUIDs = new java.util.HashSet<>(); + for (ServerPlayerEntity p : existingPlayers) { + existingUUIDs.add(p.getUuid()); + } + for (ServerPlayerEntity player : playerList) { + if (!existingUUIDs.contains(player.getUuid())) { + bossBar.addPlayer(player); + } + } + } + } + + private void finish() { + if (finished) return; + finished = true; + bossBar.setVisible(false); + bossBar.clearPlayers(); + + playSoundToAll(cx, topY + 1.0, cz, SoundEvents.BLOCK_BEACON_DEACTIVATE, SoundCategory.BLOCKS, 2.0f, 0.8f); + playSoundToAll(cx, topY + 1.0, cz, SoundEvents.BLOCK_CONDUIT_DEACTIVATE, SoundCategory.BLOCKS, 2.0f, 1.0f); + playSoundToAll(cx, topY + 1.0, cz, SoundEvents.UI_TOAST_CHALLENGE_COMPLETE, SoundCategory.MASTER, 1.5f, 1.0f); + } + } +} diff --git a/src/main/java/com/gvsds/tcme/config/Config.java b/src/main/java/com/gvsds/tcme/config/Config.java new file mode 100644 index 0000000..e9f2fdb --- /dev/null +++ b/src/main/java/com/gvsds/tcme/config/Config.java @@ -0,0 +1,69 @@ +package com.gvsds.tcme.config; + +import java.util.List; + +public class Config { + + public static class Tables { + private String currencies = "cme_currencies"; + private String balances = "cme_balances"; + private String transactions = "cme_transactions"; + private String paymentRequests = "cme_payment_requests"; + + public String getCurrencies() { return currencies; } + public void setCurrencies(String currencies) { this.currencies = currencies; } + + public String getBalances() { return balances; } + public void setBalances(String balances) { this.balances = balances; } + + public String getTransactions() { return transactions; } + public void setTransactions(String transactions) { this.transactions = transactions; } + + public String getPaymentRequests() { return paymentRequests; } + public void setPaymentRequests(String paymentRequests) { this.paymentRequests = paymentRequests; } + } + + public static class Database { + private String type = "sqlite"; + private String sqlitePath = "./config/cardinal-economy/database"; + private String mysqlHost = "localhost"; + private int mysqlPort = 3306; + private String mysqlDatabase = "cardinal_economy"; + private String mysqlUsername = "root"; + private String mysqlPassword = ""; + private boolean useSSL = false; + private Tables tables = new Tables(); + + public String getType() { return type; } + public void setType(String type) { this.type = type; } + + public String getSqlitePath() { return sqlitePath; } + public void setSqlitePath(String sqlitePath) { this.sqlitePath = sqlitePath; } + + public String getMysqlHost() { return mysqlHost; } + public void setMysqlHost(String mysqlHost) { this.mysqlHost = mysqlHost; } + + public int getMysqlPort() { return mysqlPort; } + public void setMysqlPort(int mysqlPort) { this.mysqlPort = mysqlPort; } + + public String getMysqlDatabase() { return mysqlDatabase; } + public void setMysqlDatabase(String mysqlDatabase) { this.mysqlDatabase = mysqlDatabase; } + + public String getMysqlUsername() { return mysqlUsername; } + public void setMysqlUsername(String mysqlUsername) { this.mysqlUsername = mysqlUsername; } + + public String getMysqlPassword() { return mysqlPassword; } + public void setMysqlPassword(String mysqlPassword) { this.mysqlPassword = mysqlPassword; } + + public boolean isUseSSL() { return useSSL; } + public void setUseSSL(boolean useSSL) { this.useSSL = useSSL; } + + public Tables getTables() { return tables; } + public void setTables(Tables tables) { this.tables = tables; } + } + + private Database database = new Database(); + + public Database getDatabase() { return database; } + public void setDatabase(Database database) { this.database = database; } +} diff --git a/src/main/java/com/gvsds/tcme/config/ConfigManager.java b/src/main/java/com/gvsds/tcme/config/ConfigManager.java new file mode 100644 index 0000000..0ac9b79 --- /dev/null +++ b/src/main/java/com/gvsds/tcme/config/ConfigManager.java @@ -0,0 +1,55 @@ +package com.gvsds.tcme.config; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; + +public class ConfigManager { + + private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create(); + private static final Path CONFIG_DIR = Path.of("config", "cardinal-economy"); + private static final Path CONFIG_FILE = CONFIG_DIR.resolve("config.json"); + + private Config config; + + public ConfigManager() { + loadConfig(); + } + + private void loadConfig() { + try { + if (!Files.exists(CONFIG_DIR)) { + Files.createDirectories(CONFIG_DIR); + } + + if (Files.exists(CONFIG_FILE)) { + String json = Files.readString(CONFIG_FILE); + config = GSON.fromJson(json, Config.class); + } else { + config = new Config(); + saveConfig(); + } + } catch (IOException e) { + throw new RuntimeException("Failed to load config", e); + } + } + + public void saveConfig() { + try { + if (!Files.exists(CONFIG_DIR)) { + Files.createDirectories(CONFIG_DIR); + } + String json = GSON.toJson(config); + Files.writeString(CONFIG_FILE, json); + } catch (IOException e) { + throw new RuntimeException("Failed to save config", e); + } + } + + public Config getConfig() { + return config; + } +} diff --git a/src/main/java/com/gvsds/tcme/database/DatabaseManager.java b/src/main/java/com/gvsds/tcme/database/DatabaseManager.java new file mode 100644 index 0000000..f59e7a5 --- /dev/null +++ b/src/main/java/com/gvsds/tcme/database/DatabaseManager.java @@ -0,0 +1,187 @@ +package com.gvsds.tcme.database; + +import com.gvsds.tcme.CardinalCooperativeCapitalismMarketEconomy; +import com.gvsds.tcme.config.Config; +import com.zaxxer.hikari.HikariConfig; +import com.zaxxer.hikari.HikariDataSource; + +import java.sql.Connection; +import java.sql.SQLException; + +public class DatabaseManager { + + private Config.Database dbConfig; + private boolean initialized = false; + private HikariDataSource dataSource; + + public DatabaseManager(Config.Database dbConfig) { + this.dbConfig = dbConfig; + } + + public void connect() { + try { + HikariConfig hikariConfig = new HikariConfig(); + + if ("sqlite".equalsIgnoreCase(dbConfig.getType())) { + String dbPath = dbConfig.getSqlitePath(); + java.nio.file.Path path = java.nio.file.Path.of(dbPath); + try { + java.nio.file.Files.createDirectories(path); + } catch (java.io.IOException e) { + CardinalCooperativeCapitalismMarketEconomy.LOGGER.error("Failed to create SQLite directory", e); + } + String jdbcUrl = "jdbc:sqlite:" + dbPath + "/cardinal_economy.db"; + hikariConfig.setJdbcUrl(jdbcUrl); + hikariConfig.setDriverClassName("org.sqlite.JDBC"); + hikariConfig.setMaximumPoolSize(1); + hikariConfig.setConnectionTimeout(10000); + hikariConfig.setAutoCommit(false); + } else if ("mysql".equalsIgnoreCase(dbConfig.getType())) { + String jdbcUrl = String.format("jdbc:mysql://%s:%d/%s?useSSL=%s&allowPublicKeyRetrieval=true&serverTimezone=UTC", + dbConfig.getMysqlHost(), + dbConfig.getMysqlPort(), + dbConfig.getMysqlDatabase(), + dbConfig.isUseSSL()); + hikariConfig.setJdbcUrl(jdbcUrl); + hikariConfig.setDriverClassName("com.mysql.cj.jdbc.Driver"); + hikariConfig.setUsername(dbConfig.getMysqlUsername()); + hikariConfig.setPassword(dbConfig.getMysqlPassword()); + hikariConfig.setMaximumPoolSize(10); + hikariConfig.setMinimumIdle(2); + hikariConfig.setConnectionTimeout(30000); + hikariConfig.setIdleTimeout(600000); + hikariConfig.setMaxLifetime(1800000); + hikariConfig.setAutoCommit(false); + hikariConfig.addDataSourceProperty("cachePrepStmts", "true"); + hikariConfig.addDataSourceProperty("prepStmtCacheSize", "250"); + hikariConfig.addDataSourceProperty("prepStmtCacheSqlLimit", "2048"); + } else { + CardinalCooperativeCapitalismMarketEconomy.LOGGER.error("Unknown database type: {}", dbConfig.getType()); + return; + } + + hikariConfig.setPoolName("3CME-Pool"); + this.dataSource = new HikariDataSource(hikariConfig); + + try (Connection testConn = dataSource.getConnection()) { + CardinalCooperativeCapitalismMarketEconomy.LOGGER.info("Successfully connected to {} database (HikariCP pool initialized)", dbConfig.getType()); + } + + initialized = true; + initializeTables(); + } catch (SQLException e) { + CardinalCooperativeCapitalismMarketEconomy.LOGGER.error("Failed to connect to database", e); + } + } + + private String getJdbcUrl(Config.Database dbConfig) { + if ("sqlite".equalsIgnoreCase(dbConfig.getType())) { + String dbPath = dbConfig.getSqlitePath(); + return "jdbc:sqlite:" + dbPath + "/cardinal_economy.db"; + } else if ("mysql".equalsIgnoreCase(dbConfig.getType())) { + return String.format("jdbc:mysql://%s:%d/%s?useSSL=%s&allowPublicKeyRetrieval=true&serverTimezone=UTC", + dbConfig.getMysqlHost(), + dbConfig.getMysqlPort(), + dbConfig.getMysqlDatabase(), + dbConfig.isUseSSL()); + } + return ""; + } + + private void initializeTables() { + try (Connection conn = getConnection()) { + try (var statement = conn.createStatement()) { + String dbType = dbConfig.getType(); + boolean isMySQL = "mysql".equalsIgnoreCase(dbType); + + String uuidType = isMySQL ? "BINARY(16)" : "BLOB"; + String textType = isMySQL ? "VARCHAR(255)" : "TEXT"; + String autoIncrement = isMySQL ? "AUTO_INCREMENT" : "AUTOINCREMENT"; + String currentTimestamp = isMySQL ? "CURRENT_TIMESTAMP" : "CURRENT_TIMESTAMP"; + String engineClause = isMySQL ? "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4" : ""; + + String currenciesTable = String.format(""" + CREATE TABLE IF NOT EXISTS %s ( + id INTEGER PRIMARY KEY %s, + name %s NOT NULL UNIQUE, + symbol %s NOT NULL, + tradable INTEGER DEFAULT 1, + created_at TIMESTAMP DEFAULT %s + ) %s + """, dbConfig.getTables().getCurrencies(), autoIncrement, textType, textType, currentTimestamp, engineClause); + + String balancesTable = String.format(""" + CREATE TABLE IF NOT EXISTS %s ( + id INTEGER PRIMARY KEY %s, + user_uuid %s NOT NULL, + currency_id INTEGER NOT NULL, + balance REAL DEFAULT 0.0, + created_at TIMESTAMP DEFAULT %s, + updated_at TIMESTAMP DEFAULT %s, + FOREIGN KEY (currency_id) REFERENCES %s(id), + UNIQUE(user_uuid, currency_id) + ) %s + """, dbConfig.getTables().getBalances(), autoIncrement, uuidType, currentTimestamp, currentTimestamp, + dbConfig.getTables().getCurrencies(), engineClause); + + String transactionsTable = String.format(""" + CREATE TABLE IF NOT EXISTS %s ( + id INTEGER PRIMARY KEY %s, + from_uuid %s, + to_uuid %s, + currency_id INTEGER NOT NULL, + amount REAL NOT NULL, + type %s NOT NULL, + description %s, + created_at TIMESTAMP DEFAULT %s, + FOREIGN KEY (currency_id) REFERENCES %s(id) + ) %s + """, dbConfig.getTables().getTransactions(), autoIncrement, uuidType, uuidType, + textType, textType, currentTimestamp, dbConfig.getTables().getCurrencies(), engineClause); + + String paymentRequestsTable = String.format(""" + CREATE TABLE IF NOT EXISTS %s ( + id INTEGER PRIMARY KEY %s, + from_uuid %s NOT NULL, + to_uuid %s NOT NULL, + currency_id INTEGER NOT NULL, + amount REAL NOT NULL, + status %s DEFAULT 'pending', + created_at TIMESTAMP DEFAULT %s, + FOREIGN KEY (currency_id) REFERENCES %s(id) + ) %s + """, dbConfig.getTables().getPaymentRequests(), autoIncrement, uuidType, uuidType, + textType, currentTimestamp, dbConfig.getTables().getCurrencies(), engineClause); + + statement.execute(currenciesTable); + statement.execute(balancesTable); + statement.execute(transactionsTable); + statement.execute(paymentRequestsTable); + + conn.commit(); + CardinalCooperativeCapitalismMarketEconomy.LOGGER.info("Database tables initialized"); + } + } catch (SQLException e) { + CardinalCooperativeCapitalismMarketEconomy.LOGGER.error("Failed to initialize database tables", e); + } + } + + public Connection getConnection() throws SQLException { + Connection conn = dataSource.getConnection(); + conn.setAutoCommit(false); + return conn; + } + + public void disconnect() { + initialized = false; + if (dataSource != null && !dataSource.isClosed()) { + dataSource.close(); + CardinalCooperativeCapitalismMarketEconomy.LOGGER.info("HikariCP datasource closed"); + } + CardinalCooperativeCapitalismMarketEconomy.LOGGER.info("Database manager disconnected"); + } + + public boolean isConnected() { + return initialized && dataSource != null && !dataSource.isClosed(); + } +} diff --git a/src/main/java/com/gvsds/tcme/gui/AlertScreenHandler.java b/src/main/java/com/gvsds/tcme/gui/AlertScreenHandler.java new file mode 100644 index 0000000..ab400cf --- /dev/null +++ b/src/main/java/com/gvsds/tcme/gui/AlertScreenHandler.java @@ -0,0 +1,106 @@ +package com.gvsds.tcme.gui; + +import com.gvsds.tcme.util.TextUtil; +import net.minecraft.component.DataComponentTypes; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.entity.player.PlayerInventory; +import net.minecraft.item.ItemStack; +import net.minecraft.item.Items; +import net.minecraft.network.packet.s2c.play.CloseScreenS2CPacket; +import net.minecraft.network.packet.s2c.play.ScreenHandlerSlotUpdateS2CPacket; +import net.minecraft.screen.ScreenHandler; +import net.minecraft.screen.ScreenHandlerType; +import net.minecraft.screen.slot.Slot; +import net.minecraft.screen.slot.SlotActionType; +import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.text.Text; + +import java.util.function.Consumer; + +public class AlertScreenHandler extends ScreenHandler { + private final GuiInventory inventory; + private final Consumer callback; + private boolean closed = false; + + public AlertScreenHandler(int syncId, PlayerInventory playerInventory, String message, Consumer onClose) { + super(ScreenHandlerType.GENERIC_9X3, syncId); + this.callback = onClose; + this.inventory = new GuiInventory(27); + + for (int row = 0; row < 3; ++row) { + for (int col = 0; col < 9; ++col) { + this.addSlot(new Slot(this.inventory, col + row * 9, 8 + col * 18, 18 + row * 18) { + @Override + public boolean canInsert(ItemStack stack) { + return false; + } + + @Override + public boolean canTakeItems(PlayerEntity playerEntity) { + return false; + } + }); + } + } + + ItemStack infoItem = new ItemStack(Items.PAPER); + infoItem.set(DataComponentTypes.CUSTOM_NAME, TextUtil.parseFormatting(message)); + inventory.setSlot(13, infoItem); + + ItemStack closeItem = new ItemStack(Items.RED_WOOL); + closeItem.set(DataComponentTypes.CUSTOM_NAME, Text.literal("[ Close ]").formatted(net.minecraft.util.Formatting.RED)); + inventory.setSlot(22, closeItem); + + for (int i = 0; i < 9; i++) { + this.addSlot(new Slot(playerInventory, i, 8 + i * 18, 142)); + } + } + + @Override + public ItemStack quickMove(PlayerEntity player, int index) { + return ItemStack.EMPTY; + } + + @Override + public boolean canUse(PlayerEntity player) { + return true; + } + + private void closeGui(PlayerEntity player) { + if (closed) return; + closed = true; + if (player instanceof ServerPlayerEntity serverPlayer) { + serverPlayer.networkHandler.sendPacket(new CloseScreenS2CPacket(this.syncId)); + } + } + + @Override + public void onSlotClick(int slotIndex, int button, SlotActionType actionType, PlayerEntity player) { + if (closed) return; + + if (slotIndex == 22 && actionType == SlotActionType.PICKUP) { + closeGui(player); + if (callback != null) callback.accept(true); + return; + } + + if (slotIndex >= 0 && slotIndex < 27 && actionType == SlotActionType.PICKUP) { + closeGui(player); + if (callback != null) callback.accept(true); + return; + } + + if (player instanceof ServerPlayerEntity sp) { + sp.networkHandler.sendPacket(new ScreenHandlerSlotUpdateS2CPacket(this.syncId, 0, slotIndex, this.getSlot(slotIndex).getStack())); + } + } + + @Override + public void onClosed(PlayerEntity player) { + if (!closed) { + closed = true; + if (callback != null) callback.accept(true); + } + super.onClosed(player); + } +} diff --git a/src/main/java/com/gvsds/tcme/gui/ConfirmScreenHandler.java b/src/main/java/com/gvsds/tcme/gui/ConfirmScreenHandler.java new file mode 100644 index 0000000..9d8711b --- /dev/null +++ b/src/main/java/com/gvsds/tcme/gui/ConfirmScreenHandler.java @@ -0,0 +1,117 @@ +package com.gvsds.tcme.gui; + +import com.gvsds.tcme.util.TextUtil; +import net.minecraft.component.DataComponentTypes; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.entity.player.PlayerInventory; +import net.minecraft.item.ItemStack; +import net.minecraft.item.Items; +import net.minecraft.network.packet.s2c.play.CloseScreenS2CPacket; +import net.minecraft.network.packet.s2c.play.ScreenHandlerSlotUpdateS2CPacket; +import net.minecraft.screen.ScreenHandler; +import net.minecraft.screen.ScreenHandlerType; +import net.minecraft.screen.slot.Slot; +import net.minecraft.screen.slot.SlotActionType; +import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.text.Text; +import net.minecraft.util.Formatting; + +import java.util.function.Consumer; + +public class ConfirmScreenHandler extends ScreenHandler { + private final GuiInventory inventory; + private final Consumer callback; + private boolean responded = false; + + public ConfirmScreenHandler(int syncId, PlayerInventory playerInventory, String message, Consumer onConfirm) { + super(ScreenHandlerType.GENERIC_9X3, syncId); + this.callback = onConfirm; + this.inventory = new GuiInventory(27); + + for (int row = 0; row < 3; ++row) { + for (int col = 0; col < 9; ++col) { + this.addSlot(new Slot(this.inventory, col + row * 9, 8 + col * 18, 18 + row * 18) { + @Override + public boolean canInsert(ItemStack stack) { + return false; + } + + @Override + public boolean canTakeItems(PlayerEntity playerEntity) { + return false; + } + }); + } + } + + ItemStack infoItem = new ItemStack(Items.BOOK); + infoItem.set(DataComponentTypes.CUSTOM_NAME, TextUtil.parseFormatting(message)); + inventory.setSlot(13, infoItem); + + ItemStack confirmItem = new ItemStack(Items.LIME_WOOL); + confirmItem.set(DataComponentTypes.CUSTOM_NAME, Text.literal("[ Confirm ]").formatted(Formatting.GREEN)); + inventory.setSlot(20, confirmItem); + + ItemStack cancelItem = new ItemStack(Items.RED_WOOL); + cancelItem.set(DataComponentTypes.CUSTOM_NAME, Text.literal("[ Cancel ]").formatted(Formatting.RED)); + inventory.setSlot(24, cancelItem); + + for (int i = 0; i < 9; i++) { + this.addSlot(new Slot(playerInventory, i, 8 + i * 18, 142)); + } + } + + @Override + public ItemStack quickMove(PlayerEntity player, int index) { + return ItemStack.EMPTY; + } + + @Override + public boolean canUse(PlayerEntity player) { + return true; + } + + private void closeGui(PlayerEntity player) { + if (player instanceof ServerPlayerEntity serverPlayer) { + serverPlayer.networkHandler.sendPacket(new CloseScreenS2CPacket(this.syncId)); + } + } + + @Override + public void onSlotClick(int slotIndex, int button, SlotActionType actionType, PlayerEntity player) { + if (responded) { + closeGui(player); + return; + } + + if (actionType != SlotActionType.PICKUP) { + if (player instanceof ServerPlayerEntity sp) { + sp.networkHandler.sendPacket(new ScreenHandlerSlotUpdateS2CPacket(this.syncId, 0, slotIndex, this.getSlot(slotIndex).getStack())); + } + return; + } + + if (slotIndex == 20) { + responded = true; + closeGui(player); + if (callback != null) callback.accept(true); + } else if (slotIndex == 24) { + responded = true; + closeGui(player); + if (callback != null) callback.accept(false); + } else { + if (player instanceof ServerPlayerEntity sp) { + sp.networkHandler.sendPacket(new ScreenHandlerSlotUpdateS2CPacket(this.syncId, 0, slotIndex, this.getSlot(slotIndex).getStack())); + } + } + } + + @Override + public void onClosed(PlayerEntity player) { + if (!responded) { + responded = true; + if (callback != null) callback.accept(false); + } + super.onClosed(player); + } +} diff --git a/src/main/java/com/gvsds/tcme/gui/GuiInventory.java b/src/main/java/com/gvsds/tcme/gui/GuiInventory.java new file mode 100644 index 0000000..fc2d5d4 --- /dev/null +++ b/src/main/java/com/gvsds/tcme/gui/GuiInventory.java @@ -0,0 +1,87 @@ +package com.gvsds.tcme.gui; + +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.entity.player.PlayerInventory; +import net.minecraft.inventory.Inventory; +import net.minecraft.item.ItemStack; +import net.minecraft.screen.ScreenHandler; +import net.minecraft.screen.slot.Slot; + +public class GuiInventory implements Inventory { + + private final ItemStack[] items; + + public GuiInventory(int size) { + this.items = new ItemStack[size]; + for (int i = 0; i < size; i++) { + items[i] = ItemStack.EMPTY; + } + } + + public void setSlot(int index, ItemStack stack) { + if (index >= 0 && index < items.length) { + items[index] = stack; + } + } + + public void setSlot(int index, net.minecraft.item.Item item) { + setSlot(index, new ItemStack(item)); + } + + public void setSlot(int index, net.minecraft.item.Item item, int count) { + setSlot(index, new ItemStack(item, count)); + } + + @Override + public int size() { + return items.length; + } + + @Override + public boolean isEmpty() { + for (ItemStack stack : items) { + if (!stack.isEmpty()) return false; + } + return true; + } + + @Override + public ItemStack getStack(int slot) { + if (slot >= 0 && slot < items.length) { + return items[slot]; + } + return ItemStack.EMPTY; + } + + @Override + public ItemStack removeStack(int slot, int amount) { + return ItemStack.EMPTY; + } + + @Override + public ItemStack removeStack(int slot) { + return ItemStack.EMPTY; + } + + @Override + public void setStack(int slot, ItemStack stack) { + if (slot >= 0 && slot < items.length) { + items[slot] = stack.copy(); + } + } + + @Override + public void markDirty() {} + + @Override + public boolean canPlayerUse(PlayerEntity player) { + return true; + } + + @Override + public void clear() { + for (int i = 0; i < items.length; i++) { + items[i] = ItemStack.EMPTY; + } + } +} diff --git a/src/main/java/com/gvsds/tcme/gui/InputScreenHandler.java b/src/main/java/com/gvsds/tcme/gui/InputScreenHandler.java new file mode 100644 index 0000000..008505c --- /dev/null +++ b/src/main/java/com/gvsds/tcme/gui/InputScreenHandler.java @@ -0,0 +1,151 @@ +package com.gvsds.tcme.gui; + +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.entity.player.PlayerInventory; +import net.minecraft.inventory.Inventory; +import net.minecraft.item.ItemStack; +import net.minecraft.item.Items; +import net.minecraft.network.packet.s2c.play.CloseScreenS2CPacket; +import net.minecraft.screen.AnvilScreenHandler; +import net.minecraft.screen.ScreenHandlerContext; +import net.minecraft.screen.slot.SlotActionType; +import net.minecraft.server.network.ServerPlayerEntity; + +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; +import java.util.function.Consumer; + +public class InputScreenHandler extends AnvilScreenHandler { + private static final Map sessions = new HashMap<>(); + private final UUID playerId; + private boolean closed = false; + + public InputScreenHandler(int syncId, PlayerInventory playerInventory, String hint, Consumer callback, ServerPlayerEntity sp) { + super(syncId, playerInventory, ScreenHandlerContext.create(sp.getEntityWorld(), sp.getBlockPos())); + this.playerId = sp.getUuid(); + + InputSession session = new InputSession(syncId, callback); + sessions.put(this.playerId, session); + + ItemStack hintItem = new ItemStack(Items.PAPER); + hintItem.setCount(1); + this.getSlot(0).setStack(hintItem); + } + + public static boolean hasSession(UUID playerId) { + InputSession session = sessions.get(playerId); + return session != null && !session.responded; + } + + public static void submitInput(ServerPlayerEntity player, String text) { + InputSession session = sessions.get(player.getUuid()); + if (session != null && !session.responded) { + session.responded = true; + player.networkHandler.sendPacket(new CloseScreenS2CPacket(session.syncId)); + if (session.callback != null) { + session.callback.accept(text); + } + sessions.remove(player.getUuid()); + } + } + + @Override + public void onSlotClick(int slotIndex, int button, SlotActionType actionType, PlayerEntity player) { + if (slotIndex == 2) { + if (player instanceof ServerPlayerEntity sp && !closed) { + ItemStack result = this.getSlot(2).getStack(); + if (!result.isEmpty()) { + closed = true; + InputSession session = sessions.get(sp.getUuid()); + if (session != null && !session.responded) { + session.responded = true; + String text = result.getName().getString(); + if (session.callback != null) { + session.callback.accept(text); + } + sp.networkHandler.sendPacket(new CloseScreenS2CPacket(session.syncId)); + sessions.remove(sp.getUuid()); + } + } + } + return; + } + if (slotIndex == 0 || slotIndex == 1) { + return; + } + super.onSlotClick(slotIndex, button, actionType, player); + } + + @Override + public void onClosed(PlayerEntity player) { + if (player instanceof ServerPlayerEntity sp) { + InputSession session = sessions.get(sp.getUuid()); + if (session != null && !session.responded) { + session.responded = true; + if (session.callback != null) { + session.callback.accept(null); + } + } + this.getSlot(0).setStack(ItemStack.EMPTY); + this.getSlot(1).setStack(ItemStack.EMPTY); + this.getSlot(2).setStack(ItemStack.EMPTY); + sessions.remove(sp.getUuid()); + } + super.onClosed(player); + } + + @Override + public ItemStack quickMove(PlayerEntity player, int index) { + return ItemStack.EMPTY; + } + + @Override + public boolean canUse(PlayerEntity player) { + return true; + } + + @Override + public boolean canTakeOutput(PlayerEntity player, boolean present) { + return false; + } + + @Override + public boolean onButtonClick(PlayerEntity player, int id) { + if (id == 0 && !closed) { + ItemStack result = this.getSlot(2).getStack(); + if (!result.isEmpty()) { + if (player instanceof ServerPlayerEntity sp) { + closed = true; + InputSession session = sessions.get(sp.getUuid()); + if (session != null && !session.responded) { + session.responded = true; + String text = result.getName().getString(); + if (session.callback != null) { + session.callback.accept(text); + } + sp.networkHandler.sendPacket(new CloseScreenS2CPacket(session.syncId)); + sessions.remove(sp.getUuid()); + return true; + } + } + } + } + return false; + } + + @Override + public void onTakeOutput(PlayerEntity player, ItemStack stack) { + } + + public static class InputSession { + public final int syncId; + public boolean responded; + public final Consumer callback; + + public InputSession(int syncId, Consumer callback) { + this.syncId = syncId; + this.callback = callback; + } + } +} diff --git a/src/main/java/com/gvsds/tcme/gui/LargeChestScreenHandler.java b/src/main/java/com/gvsds/tcme/gui/LargeChestScreenHandler.java new file mode 100644 index 0000000..01d32ea --- /dev/null +++ b/src/main/java/com/gvsds/tcme/gui/LargeChestScreenHandler.java @@ -0,0 +1,66 @@ +package com.gvsds.tcme.gui; + +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.entity.player.PlayerInventory; +import net.minecraft.item.ItemStack; +import net.minecraft.network.packet.s2c.play.ScreenHandlerSlotUpdateS2CPacket; +import net.minecraft.screen.ScreenHandler; +import net.minecraft.screen.ScreenHandlerType; +import net.minecraft.screen.slot.Slot; +import net.minecraft.screen.slot.SlotActionType; +import net.minecraft.server.network.ServerPlayerEntity; + +public class LargeChestScreenHandler extends ScreenHandler { + private final GuiInventory inventory; + + public LargeChestScreenHandler(int syncId, PlayerInventory playerInventory, GuiInventory guiInventory) { + super(ScreenHandlerType.GENERIC_9X6, syncId); + this.inventory = guiInventory; + + for (int row = 0; row < 6; ++row) { + for (int col = 0; col < 9; ++col) { + this.addSlot(new Slot(this.inventory, col + row * 9, 8 + col * 18, 18 + row * 18) { + @Override + public boolean canInsert(ItemStack stack) { + return false; + } + + @Override + public boolean canTakeItems(PlayerEntity playerEntity) { + return false; + } + }); + } + } + + for (int row = 0; row < 3; ++row) { + for (int col = 0; col < 9; ++col) { + this.addSlot(new Slot(playerInventory, col + row * 9 + 9, 8 + col * 18, 140 + row * 18)); + } + } + + for (int i = 0; i < 9; i++) { + this.addSlot(new Slot(playerInventory, i, 8 + i * 18, 198)); + } + } + + @Override + public ItemStack quickMove(PlayerEntity player, int index) { + return ItemStack.EMPTY; + } + + @Override + public boolean canUse(PlayerEntity player) { + return true; + } + + @Override + public void onSlotClick(int slotIndex, int button, SlotActionType actionType, PlayerEntity player) { + if (player instanceof ServerPlayerEntity sp) { + if (slotIndex >= 0 && slotIndex < 54) { + sp.networkHandler.sendPacket(new ScreenHandlerSlotUpdateS2CPacket(this.syncId, 0, slotIndex, this.getSlot(slotIndex).getStack())); + } + sp.networkHandler.sendPacket(new ScreenHandlerSlotUpdateS2CPacket(this.syncId, -1, -1, ItemStack.EMPTY)); + } + } +} diff --git a/src/main/java/com/gvsds/tcme/gui/ServerGuiAPI.java b/src/main/java/com/gvsds/tcme/gui/ServerGuiAPI.java new file mode 100644 index 0000000..dc9a959 --- /dev/null +++ b/src/main/java/com/gvsds/tcme/gui/ServerGuiAPI.java @@ -0,0 +1,134 @@ +package com.gvsds.tcme.gui; + +import com.gvsds.tcme.util.TextUtil; +import net.minecraft.entity.player.PlayerInventory; +import net.minecraft.screen.GenericContainerScreenHandler; +import net.minecraft.screen.NamedScreenHandlerFactory; +import net.minecraft.screen.ScreenHandler; +import net.minecraft.screen.AnvilScreenHandler; +import net.minecraft.screen.ScreenHandlerContext; +import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.text.Text; +import org.jetbrains.annotations.Nullable; + +import java.util.function.Consumer; + +public class ServerGuiAPI { + + public static void showAlert(ServerPlayerEntity player, String title, String message) { + showAlert(player, title, message, null); + } + + public static void showAlert(ServerPlayerEntity player, String title, String message, Consumer onClose) { + NamedScreenHandlerFactory factory = new NamedScreenHandlerFactory() { + @Override + public Text getDisplayName() { + return TextUtil.parseFormatting(title); + } + + @Nullable + @Override + public ScreenHandler createMenu(int syncId, PlayerInventory playerInv, net.minecraft.entity.player.PlayerEntity player) { + return new AlertScreenHandler(syncId, playerInv, message, onClose); + } + }; + player.openHandledScreen(factory); + } + + public static void showConfirm(ServerPlayerEntity player, String title, String message, Consumer onConfirm) { + NamedScreenHandlerFactory factory = new NamedScreenHandlerFactory() { + @Override + public Text getDisplayName() { + return TextUtil.parseFormatting(title); + } + + @Nullable + @Override + public ScreenHandler createMenu(int syncId, PlayerInventory playerInv, net.minecraft.entity.player.PlayerEntity player) { + return new ConfirmScreenHandler(syncId, playerInv, message, onConfirm); + } + }; + player.openHandledScreen(factory); + } + + public static void showInputBox(ServerPlayerEntity player, String title, String hint, Consumer onSubmit) { + NamedScreenHandlerFactory factory = new NamedScreenHandlerFactory() { + @Override + public Text getDisplayName() { + return TextUtil.parseFormatting(title); + } + + @Nullable + @Override + public ScreenHandler createMenu(int syncId, PlayerInventory playerInv, net.minecraft.entity.player.PlayerEntity player) { + return new InputScreenHandler(syncId, playerInv, hint, onSubmit, player instanceof ServerPlayerEntity ? (ServerPlayerEntity) player : null); + } + }; + player.openHandledScreen(factory); + } + + public static void showSmallChest(ServerPlayerEntity player, String title, Consumer onSlotClick) { + GuiInventory inventory = new GuiInventory(9); + player.openHandledScreen(new NamedScreenHandlerFactory() { + @Override + public Text getDisplayName() { + return TextUtil.parseFormatting(title); + } + + @Nullable + @Override + public ScreenHandler createMenu(int syncId, PlayerInventory playerInv, net.minecraft.entity.player.PlayerEntity player) { + return new SmallChestScreenHandler(syncId, playerInv, inventory); + } + }); + } + + public static void showMediumChest(ServerPlayerEntity player, String title, Consumer onSlotClick) { + GuiInventory inventory = new GuiInventory(27); + player.openHandledScreen(new NamedScreenHandlerFactory() { + @Override + public Text getDisplayName() { + return TextUtil.parseFormatting(title); + } + + @Nullable + @Override + public ScreenHandler createMenu(int syncId, PlayerInventory playerInv, net.minecraft.entity.player.PlayerEntity player) { + return GenericContainerScreenHandler.createGeneric9x3(syncId, playerInv, inventory); + } + }); + } + + public static void showLargeChest(ServerPlayerEntity player, String title, Consumer onSlotClick) { + GuiInventory inventory = new GuiInventory(54); + player.openHandledScreen(new NamedScreenHandlerFactory() { + @Override + public Text getDisplayName() { + return TextUtil.parseFormatting(title); + } + + @Nullable + @Override + public ScreenHandler createMenu(int syncId, PlayerInventory playerInv, net.minecraft.entity.player.PlayerEntity player) { + return new LargeChestScreenHandler(syncId, playerInv, inventory); + } + }); + } + + public static void showAnvil(ServerPlayerEntity player, String title) { + player.openHandledScreen(new NamedScreenHandlerFactory() { + @Override + public Text getDisplayName() { + return TextUtil.parseFormatting(title); + } + + @Nullable + @Override + public ScreenHandler createMenu(int syncId, PlayerInventory playerInv, net.minecraft.entity.player.PlayerEntity player) { + ServerPlayerEntity sp = (ServerPlayerEntity) player; + return new AnvilScreenHandler(syncId, playerInv, + ScreenHandlerContext.create(sp.getEntityWorld(), sp.getBlockPos())); + } + }); + } +} diff --git a/src/main/java/com/gvsds/tcme/gui/SmallChestScreenHandler.java b/src/main/java/com/gvsds/tcme/gui/SmallChestScreenHandler.java new file mode 100644 index 0000000..6de761c --- /dev/null +++ b/src/main/java/com/gvsds/tcme/gui/SmallChestScreenHandler.java @@ -0,0 +1,60 @@ +package com.gvsds.tcme.gui; + +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.entity.player.PlayerInventory; +import net.minecraft.item.ItemStack; +import net.minecraft.network.packet.s2c.play.ScreenHandlerSlotUpdateS2CPacket; +import net.minecraft.screen.ScreenHandler; +import net.minecraft.screen.ScreenHandlerType; +import net.minecraft.screen.slot.Slot; +import net.minecraft.screen.slot.SlotActionType; +import net.minecraft.server.network.ServerPlayerEntity; + +public class SmallChestScreenHandler extends ScreenHandler { + private final GuiInventory inventory; + + public SmallChestScreenHandler(int syncId, PlayerInventory playerInventory, GuiInventory guiInventory) { + super(ScreenHandlerType.GENERIC_9X1, syncId); + this.inventory = guiInventory; + + for (int col = 0; col < 9; ++col) { + this.addSlot(new Slot(this.inventory, col, 8 + col * 18, 18) { + @Override + public boolean canInsert(ItemStack stack) { + return false; + } + + @Override + public boolean canTakeItems(PlayerEntity playerEntity) { + return false; + } + }); + } + + for (int row = 0; row < 3; ++row) { + for (int col = 0; col < 9; ++col) { + this.addSlot(new Slot(playerInventory, col + row * 9 + 9, 8 + col * 18, 84 + row * 18)); + } + } + } + + @Override + public ItemStack quickMove(PlayerEntity player, int index) { + return ItemStack.EMPTY; + } + + @Override + public boolean canUse(PlayerEntity player) { + return true; + } + + @Override + public void onSlotClick(int slotIndex, int button, SlotActionType actionType, PlayerEntity player) { + if (player instanceof ServerPlayerEntity sp) { + if (slotIndex >= 0 && slotIndex < 9) { + sp.networkHandler.sendPacket(new ScreenHandlerSlotUpdateS2CPacket(this.syncId, 0, slotIndex, this.getSlot(slotIndex).getStack())); + } + sp.networkHandler.sendPacket(new ScreenHandlerSlotUpdateS2CPacket(this.syncId, -1, -1, ItemStack.EMPTY)); + } + } +} diff --git a/src/main/java/com/gvsds/tcme/i18n/LanguageManager.java b/src/main/java/com/gvsds/tcme/i18n/LanguageManager.java new file mode 100644 index 0000000..0be3105 --- /dev/null +++ b/src/main/java/com/gvsds/tcme/i18n/LanguageManager.java @@ -0,0 +1,225 @@ +package com.gvsds.tcme.i18n; + +import com.gvsds.tcme.CardinalCooperativeCapitalismMarketEconomy; +import com.gvsds.tcme.database.DatabaseManager; + +import java.nio.ByteBuffer; +import java.sql.*; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; + +public class LanguageManager { + + private static final Map> LANGUAGES = new HashMap<>(); + + private final Map playerLanguages = new HashMap<>(); + private DatabaseManager dbManager; + private boolean isMySQL; + + public LanguageManager(DatabaseManager dbManager, boolean isMySQL) { + this.dbManager = dbManager; + this.isMySQL = isMySQL; + loadLanguages(); + loadAllPlayerLanguages(); + } + + private void loadLanguages() { + Map enUS = new HashMap<>(); + enUS.put("cme.wallet.title", "=== Your Wallet ==="); + enUS.put("cme.wallet.balance", " %s: %.2f %s"); + enUS.put("cme.wallet.empty", "Your wallet is empty."); + enUS.put("cme.lang.changed", "Language changed to %s"); + enUS.put("cme.lang.available", "Available languages: %s"); + enUS.put("cme.lang.notfound", "Language not found: %s"); + enUS.put("cme.currency.created", "Created currency: %s (%s) - Tradable: %s"); + enUS.put("cme.currency.create_failed", "Failed to create currency: %s"); + enUS.put("cme.currency.deleted", "Deleted currency: %s"); + enUS.put("cme.currency.delete_failed", "Failed to delete currency: %s"); + enUS.put("cme.currency.tradable", "Currency %s tradable set to: %s"); + enUS.put("cme.currency.tradable_failed", "Failed to set tradable for currency: %s"); + enUS.put("cme.currency.list", "Currencies:"); + enUS.put("cme.currency.not_found", "Currency not found: %s"); + enUS.put("cme.currency.no_currencies", "No currencies found"); + enUS.put("cme.currency.no_available", "No currencies available"); + enUS.put("cme.currency.not_tradable", "Currency %s does not allow free trading"); + enUS.put("cme.balance.check", "%s's balance: %.2f %s"); + enUS.put("cme.balance.added", "Added %.2f %s to %s"); + enUS.put("cme.balance.removed", "Removed %.2f %s from %s"); + enUS.put("cme.balance.failed", "Failed to %s balance"); + enUS.put("cme.pay.success", "Successfully paid %.2f %s to %s"); + enUS.put("cme.pay.received", "%s paid you %.2f %s"); + enUS.put("cme.pay.failed", "Payment failed. Insufficient balance or other error"); + enUS.put("cme.pay.self", "Cannot pay yourself"); + enUS.put("cme.apay.sent", "Payment request sent to %s: %.2f %s [ID: %s]"); + enUS.put("cme.apay.received", "%s requested %.2f %s from you [ID: %s]"); + enUS.put("cme.apay.self", "Cannot request payment from yourself"); + enUS.put("cme.apay.failed", "Failed to create payment request"); + enUS.put("cme.requests.list", "Pending payment requests:"); + enUS.put("cme.requests.empty", "No pending payment requests"); + enUS.put("cme.requests.item", " [ID: %s] Requested: %.2f (Currency: %s)"); + enUS.put("cme.requests.accepted", "Payment request #%s accepted"); + enUS.put("cme.requests.accept_failed", "Failed to accept payment request #%s"); + enUS.put("cme.requests.cancelled", "Payment request #%s cancelled"); + enUS.put("cme.requests.cancel_failed", "Failed to cancel payment request #%s"); + enUS.put("cme.currency.renamed", "Renamed currency: %s -> %s"); + enUS.put("cme.currency.rename_failed", "Failed to rename currency: %s"); + enUS.put("cme.currency.updated", "Updated currency %s: symbol=%s, tradable=%s"); + enUS.put("cme.currency.update_failed", "Failed to update currency: %s"); + enUS.put("cme.currency.list_item", " %s (%s) - Tradable: %s - Rate: %.2f"); + enUS.put("cme.lang.usage", "Use /cme lang to change"); + enUS.put("cme.exchange_rate.set", "Exchange rate for %s set to: %.2f"); + enUS.put("cme.exchange_rate.list", "Exchange Rates (relative to base):"); + enUS.put("cme.exchange_rate.item", " %s: %.2f"); + enUS.put("cme.convert.invalid_amount", "Amount must be greater than 0"); + enUS.put("cme.convert.same_currency", "Cannot convert to the same currency"); + enUS.put("cme.convert.insufficient", "Insufficient balance. You have %s %s, but need %s"); + enUS.put("cme.convert.title", "=== Currency Conversion Preview ==="); + enUS.put("cme.convert.from", " From: %s %s (%s)"); + enUS.put("cme.convert.to", " To: %s %s (%s)"); + enUS.put("cme.convert.rate", " Rate: %s : %s = %.2f : %.2f"); + enUS.put("cme.convert.balance_from", " Current %s balance: %s"); + enUS.put("cme.convert.balance_to", " Total %s balance after conversion: %s"); + enUS.put("cme.convert.prompt", "Use /cme convert %s %s %s confirm to proceed"); + enUS.put("cme.convert.success", "Successfully converted %s %s to %s %s"); + enUS.put("cme.convert.failed", "Conversion failed. Please try again"); + enUS.put("cme.convert.refunded", "Conversion failed. Your balance has been refunded"); + enUS.put("cme.requests.rejected", "Rejected payment request #%s"); + enUS.put("cme.requests.reject_failed", "Failed to reject payment request #%s"); + enUS.put("cme.requests.rejected_by", "%s rejected your payment request #%s: %.2f %s"); + enUS.put("cme.requests.revoked", "Revoked payment request #%s"); + enUS.put("cme.requests.revoke_failed", "Failed to revoke payment request #%s"); + enUS.put("cme.requests.revoked_by", "%s revoked payment request #%s: %.2f %s"); + enUS.put("cme.requests.accepted_by", "%s accepted your payment request #%s: %.2f %s"); + enUS.put("cme.requests.not_yours", "This is not your request"); + enUS.put("cme.requests.not_pending", "This request is no longer pending"); + LANGUAGES.put("en_us", enUS); + + Map zhCN = new HashMap<>(); + zhCN.put("cme.wallet.title", "=== 你的钱包 ==="); + zhCN.put("cme.wallet.balance", " %s: %.2f %s"); + zhCN.put("cme.wallet.empty", "你的钱包是空的。"); + zhCN.put("cme.lang.changed", "语言已更改为 %s"); + zhCN.put("cme.lang.available", "可用语言: %s"); + zhCN.put("cme.lang.notfound", "未找到语言: %s"); + zhCN.put("cme.currency.created", "创建币种: %s (%s) - 可自由交易: %s"); + zhCN.put("cme.currency.create_failed", "创建币种失败: %s"); + zhCN.put("cme.currency.deleted", "已删除ID为 %s 的币种"); + zhCN.put("cme.currency.delete_failed", "删除币种失败: %s"); + zhCN.put("cme.currency.tradable", "币种 %s 的自由交易设置为: %s"); + zhCN.put("cme.currency.tradable_failed", "设置币种自由交易失败: %s"); + zhCN.put("cme.currency.list", "币种列表:"); + zhCN.put("cme.currency.not_found", "未找到币种: %s"); + zhCN.put("cme.currency.no_currencies", "未找到任何币种"); + zhCN.put("cme.currency.no_available", "没有可用的币种"); + zhCN.put("cme.currency.not_tradable", "币种 %s 不允许自由交易"); + zhCN.put("cme.balance.check", "%s 的余额: %.2f %s"); + zhCN.put("cme.balance.added", "已添加 %.2f %s 到 %s"); + zhCN.put("cme.balance.removed", "已移除 %.2f %s (来自 %s)"); + zhCN.put("cme.balance.failed", "余额操作失败"); + zhCN.put("cme.pay.success", "成功支付 %.2f %s 给 %s"); + zhCN.put("cme.pay.received", "%s 向你支付了 %.2f %s"); + zhCN.put("cme.pay.failed", "支付失败,余额不足或其他错误"); + zhCN.put("cme.pay.self", "不能支付给自己"); + zhCN.put("cme.apay.sent", "已向 %s 发送支付请求: %.2f %s [ID: %s]"); + zhCN.put("cme.apay.received", "%s 请求你支付 %.2f %s [ID: %s]"); + zhCN.put("cme.apay.self", "不能向自己请求支付"); + zhCN.put("cme.apay.failed", "创建支付请求失败"); + zhCN.put("cme.requests.list", "待处理的支付请求:"); + zhCN.put("cme.requests.empty", "没有待处理的支付请求"); + zhCN.put("cme.requests.item", " [ID: %s] 请求金额: %.2f (币种: %s)"); + zhCN.put("cme.requests.accepted", "已接受支付请求 #%s"); + zhCN.put("cme.requests.accept_failed", "接受支付请求 #%s 失败"); + zhCN.put("cme.requests.cancelled", "已取消支付请求 #%s"); + zhCN.put("cme.requests.cancel_failed", "取消支付请求 #%s 失败"); + zhCN.put("cme.currency.renamed", "已将货币从 %s 重命名为 %s"); + zhCN.put("cme.currency.rename_failed", "重命名货币 %s 失败"); + zhCN.put("cme.currency.updated", "已更新货币 %s: 符号=%s, 可交易=%s"); + zhCN.put("cme.currency.update_failed", "更新货币 %s 失败"); + zhCN.put("cme.currency.list_item", " %s (%s) - 可交易: %s - 汇率: %.2f"); + zhCN.put("cme.lang.usage", "使用 /cme lang <语言> 来切换语言"); + zhCN.put("cme.exchange_rate.set", "%s 的汇率已设置为: %.2f"); + zhCN.put("cme.exchange_rate.list", "汇率列表(相对基准汇率):"); + zhCN.put("cme.exchange_rate.item", " %s: %.2f"); + zhCN.put("cme.convert.invalid_amount", "数量必须大于 0"); + zhCN.put("cme.convert.same_currency", "不能转换为相同的货币"); + zhCN.put("cme.convert.insufficient", "余额不足。你有 %s %s,但需要 %s"); + zhCN.put("cme.convert.title", "=== 货币转换预览 ==="); + zhCN.put("cme.convert.from", " 来源: %s %s (%s)"); + zhCN.put("cme.convert.to", " 目标: %s %s (%s)"); + zhCN.put("cme.convert.rate", " 汇率: %s : %s = %.2f : %.2f"); + zhCN.put("cme.convert.balance_from", " 当前 %s 余额: %s"); + zhCN.put("cme.convert.balance_to", " 转换后 %s 总余额: %s"); + zhCN.put("cme.convert.prompt", "使用 /cme convert %s %s %s confirm 确认执行"); + zhCN.put("cme.convert.success", "成功将 %s %s 转换为 %s %s"); + zhCN.put("cme.convert.failed", "转换失败,请重试"); + zhCN.put("cme.convert.refunded", "转换失败,已退还扣除的货币"); + zhCN.put("cme.requests.rejected", "已拒绝付款请求 #%s"); + zhCN.put("cme.requests.reject_failed", "拒绝付款请求 #%s 失败"); + zhCN.put("cme.requests.rejected_by", "%s 拒绝了你的付款请求 #%s: %.2f %s"); + zhCN.put("cme.requests.revoked", "已撤回付款请求 #%s"); + zhCN.put("cme.requests.revoke_failed", "撤回付款请求 #%s 失败"); + zhCN.put("cme.requests.revoked_by", "%s 撤回了付款请求 #%s: %.2f %s"); + zhCN.put("cme.requests.accepted_by", "%s 已接受你的付款请求 #%s: %.2f %s"); + zhCN.put("cme.requests.not_yours", "这不是你的请求"); + zhCN.put("cme.requests.not_pending", "该请求已不在待处理状态"); + LANGUAGES.put("zh_cn", zhCN); + } + + private void loadAllPlayerLanguages() { + if (dbManager == null || !dbManager.isConnected()) { + CardinalCooperativeCapitalismMarketEconomy.LOGGER.warn("Database not connected, cannot load player languages"); + return; + } + + String sql = "SELECT user_uuid, language FROM cme_player_settings"; + try (Connection conn = dbManager.getConnection(); + Statement stmt = conn.createStatement(); + ResultSet rs = stmt.executeQuery(sql)) { + while (rs.next()) { + byte[] uuidBytes = rs.getBytes("user_uuid"); + if (uuidBytes != null && uuidBytes.length == 16) { + UUID uuid = bytesToUUID(uuidBytes); + String lang = rs.getString("language"); + playerLanguages.put(uuid, lang.toLowerCase()); + } + } + CardinalCooperativeCapitalismMarketEconomy.LOGGER.info("Loaded {} player language settings", playerLanguages.size()); + } catch (SQLException e) { + CardinalCooperativeCapitalismMarketEconomy.LOGGER.error("Failed to load player languages", e); + } + } + + public void setPlayerLanguage(UUID playerUUID, String language) { + playerLanguages.put(playerUUID, language.toLowerCase()); + } + + public String getPlayerLanguage(UUID playerUUID) { + if (playerUUID == null) { + return "en_us"; + } + return playerLanguages.getOrDefault(playerUUID, "en_us"); + } + + public String getMessageRaw(UUID playerUUID, String key) { + String lang = getPlayerLanguage(playerUUID); + Map langMap = LANGUAGES.getOrDefault(lang, LANGUAGES.get("en_us")); + return langMap.getOrDefault(key, key); + } + + public boolean hasLanguage(String lang) { + return LANGUAGES.containsKey(lang.toLowerCase()); + } + + public String getAvailableLanguages() { + return String.join(", ", LANGUAGES.keySet()); + } + + private UUID bytesToUUID(byte[] bytes) { + ByteBuffer buffer = ByteBuffer.wrap(bytes); + long mostSigBits = buffer.getLong(); + long leastSigBits = buffer.getLong(); + return new UUID(mostSigBits, leastSigBits); + } +} diff --git a/src/main/java/com/gvsds/tcme/manager/BalanceManager.java b/src/main/java/com/gvsds/tcme/manager/BalanceManager.java new file mode 100644 index 0000000..3799cc0 --- /dev/null +++ b/src/main/java/com/gvsds/tcme/manager/BalanceManager.java @@ -0,0 +1,157 @@ +package com.gvsds.tcme.manager; + +import com.gvsds.tcme.CardinalCooperativeCapitalismMarketEconomy; +import com.gvsds.tcme.config.Config; +import com.gvsds.tcme.database.DatabaseManager; +import com.gvsds.tcme.model.PlayerBalance; + +import java.nio.ByteBuffer; +import java.sql.*; +import java.util.Optional; +import java.util.UUID; + +public class BalanceManager { + + private final DatabaseManager dbManager; + private final Config.Tables tables; + private final boolean isMySQL; + + public BalanceManager(DatabaseManager dbManager, Config.Tables tables, boolean isMySQL) { + this.dbManager = dbManager; + this.tables = tables; + this.isMySQL = isMySQL; + } + + public double getBalance(UUID playerUUID, int currencyId) { + String sql = String.format("SELECT balance FROM %s WHERE user_uuid = ? AND currency_id = ?", tables.getBalances()); + Connection conn = null; + try { + conn = dbManager.getConnection(); + try (PreparedStatement stmt = conn.prepareStatement(sql)) { + setUUIDParameter(stmt, 1, playerUUID); + stmt.setInt(2, currencyId); + try (ResultSet rs = stmt.executeQuery()) { + if (rs.next()) { + return rs.getDouble("balance"); + } + } + } + conn.commit(); + } catch (SQLException e) { + CardinalCooperativeCapitalismMarketEconomy.LOGGER.error("Failed to get balance for player: {}", playerUUID, e); + if (conn != null) { try { conn.rollback(); } catch (SQLException ignored) {} } + } finally { + if (conn != null) { try { conn.close(); } catch (SQLException ignored) {} } + } + return 0.0; + } + + public boolean setBalance(UUID playerUUID, int currencyId, double amount) { + Connection conn = null; + try { + conn = dbManager.getConnection(); + boolean result = setBalance(conn, playerUUID, currencyId, amount); + if (result) { + conn.commit(); + } else { + conn.rollback(); + } + return result; + } catch (SQLException e) { + CardinalCooperativeCapitalismMarketEconomy.LOGGER.error("Failed to set balance for player: {}", playerUUID, e); + if (conn != null) { try { conn.rollback(); } catch (SQLException ignored) {} } + } finally { + if (conn != null) { try { conn.close(); } catch (SQLException ignored) {} } + } + return false; + } + + public boolean addBalance(UUID playerUUID, int currencyId, double amount) { + double currentBalance = getBalance(playerUUID, currencyId); + return setBalance(playerUUID, currencyId, currentBalance + amount); + } + + public double getBalance(Connection conn, UUID playerUUID, int currencyId) throws SQLException { + String sql = String.format("SELECT balance FROM %s WHERE user_uuid = ? AND currency_id = ?", tables.getBalances()); + try (PreparedStatement stmt = conn.prepareStatement(sql)) { + setUUIDParameter(stmt, 1, playerUUID); + stmt.setInt(2, currencyId); + try (ResultSet rs = stmt.executeQuery()) { + if (rs.next()) { + return rs.getDouble("balance"); + } + } + } + return 0.0; + } + + public boolean setBalance(Connection conn, UUID playerUUID, int currencyId, double amount) throws SQLException { + String sql; + if (isMySQL) { + sql = String.format( + "INSERT INTO %s (user_uuid, currency_id, balance, created_at, updated_at) VALUES (?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP) " + + "ON DUPLICATE KEY UPDATE balance = VALUES(balance), updated_at = CURRENT_TIMESTAMP", + tables.getBalances() + ); + } else { + sql = String.format( + "INSERT INTO %s (user_uuid, currency_id, balance, created_at, updated_at) VALUES (?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP) " + + "ON CONFLICT(user_uuid, currency_id) DO UPDATE SET balance = excluded.balance, updated_at = CURRENT_TIMESTAMP", + tables.getBalances() + ); + } + try (PreparedStatement stmt = conn.prepareStatement(sql)) { + setUUIDParameter(stmt, 1, playerUUID); + stmt.setInt(2, currencyId); + stmt.setDouble(3, amount); + stmt.executeUpdate(); + return true; + } + } + + public boolean addBalance(Connection conn, UUID playerUUID, int currencyId, double amount) throws SQLException { + double currentBalance = getBalance(conn, playerUUID, currencyId); + return setBalance(conn, playerUUID, currencyId, currentBalance + amount); + } + + public boolean subtractBalance(UUID playerUUID, int currencyId, double amount) { + double currentBalance = getBalance(playerUUID, currencyId); + if (currentBalance < amount) { + return false; + } + return setBalance(playerUUID, currencyId, currentBalance - amount); + } + + public boolean hasSufficientBalance(UUID playerUUID, int currencyId, double amount) { + return getBalance(playerUUID, currencyId) >= amount; + } + + private void setUUIDParameter(PreparedStatement stmt, int parameterIndex, UUID uuid) throws SQLException { + ByteBuffer buffer = ByteBuffer.wrap(new byte[16]); + buffer.putLong(uuid.getMostSignificantBits()); + buffer.putLong(uuid.getLeastSignificantBits()); + stmt.setBytes(parameterIndex, buffer.array()); + } + + public UUID getUUIDFromResultSet(ResultSet rs, String columnName) throws SQLException { + byte[] bytes = rs.getBytes(columnName); + if (bytes == null) { + return null; + } + return bytesToUUID(bytes); + } + + private byte[] uuidToBytes(UUID uuid) { + ByteBuffer buffer = ByteBuffer.wrap(new byte[16]); + buffer.putLong(uuid.getMostSignificantBits()); + buffer.putLong(uuid.getLeastSignificantBits()); + return buffer.array(); + } + + private UUID bytesToUUID(byte[] bytes) { + ByteBuffer buffer = ByteBuffer.wrap(bytes); + long mostSigBits = buffer.getLong(); + long leastSigBits = buffer.getLong(); + return new UUID(mostSigBits, leastSigBits); + } +} diff --git a/src/main/java/com/gvsds/tcme/manager/CurrencyManager.java b/src/main/java/com/gvsds/tcme/manager/CurrencyManager.java new file mode 100644 index 0000000..45b88f9 --- /dev/null +++ b/src/main/java/com/gvsds/tcme/manager/CurrencyManager.java @@ -0,0 +1,286 @@ +package com.gvsds.tcme.manager; + +import com.gvsds.tcme.CardinalCooperativeCapitalismMarketEconomy; +import com.gvsds.tcme.config.Config; +import com.gvsds.tcme.database.DatabaseManager; +import com.gvsds.tcme.model.Currency; + +import java.sql.*; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +public class CurrencyManager { + + private final DatabaseManager dbManager; + private final Config.Tables tables; + + public CurrencyManager(DatabaseManager dbManager, Config.Tables tables) { + this.dbManager = dbManager; + this.tables = tables; + } + + public Optional createCurrency(String name, String symbol, boolean tradable) { + String sql = String.format("INSERT INTO %s (name, symbol, tradable) VALUES (?, ?, ?)", tables.getCurrencies()); + Connection conn = null; + try { + conn = dbManager.getConnection(); + try (PreparedStatement stmt = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) { + stmt.setString(1, name); + stmt.setString(2, symbol); + stmt.setInt(3, tradable ? 1 : 0); + stmt.executeUpdate(); + + try (ResultSet rs = stmt.getGeneratedKeys()) { + if (rs.next()) { + Currency currency = new Currency(name, symbol, tradable); + currency.setId(rs.getInt(1)); + CardinalCooperativeCapitalismMarketEconomy.LOGGER.info("Created currency: {} ({})", name, symbol); + conn.commit(); + return Optional.of(currency); + } + } + } + conn.rollback(); + } catch (SQLException e) { + CardinalCooperativeCapitalismMarketEconomy.LOGGER.error("Failed to create currency: {}", name, e); + if (conn != null) { try { conn.rollback(); } catch (SQLException ignored) {} } + } finally { + if (conn != null) { try { conn.close(); } catch (SQLException ignored) {} } + } + return Optional.empty(); + } + + public boolean deleteCurrency(String name) { + String sql = String.format("DELETE FROM %s WHERE name = ?", tables.getCurrencies()); + Connection conn = null; + try { + conn = dbManager.getConnection(); + try (PreparedStatement stmt = conn.prepareStatement(sql)) { + stmt.setString(1, name); + int rows = stmt.executeUpdate(); + if (rows > 0) { + conn.commit(); + CardinalCooperativeCapitalismMarketEconomy.LOGGER.info("Deleted currency: {}", name); + return true; + } + } + conn.rollback(); + } catch (SQLException e) { + CardinalCooperativeCapitalismMarketEconomy.LOGGER.error("Failed to delete currency: {}", name, e); + if (conn != null) { try { conn.rollback(); } catch (SQLException ignored) {} } + } finally { + if (conn != null) { try { conn.close(); } catch (SQLException ignored) {} } + } + return false; + } + + public boolean deleteCurrency(int currencyId) { + String sql = String.format("DELETE FROM %s WHERE id = ?", tables.getCurrencies()); + Connection conn = null; + try { + conn = dbManager.getConnection(); + try (PreparedStatement stmt = conn.prepareStatement(sql)) { + stmt.setInt(1, currencyId); + int rows = stmt.executeUpdate(); + if (rows > 0) { + conn.commit(); + CardinalCooperativeCapitalismMarketEconomy.LOGGER.info("Deleted currency with ID: {}", currencyId); + return true; + } + } + conn.rollback(); + } catch (SQLException e) { + CardinalCooperativeCapitalismMarketEconomy.LOGGER.error("Failed to delete currency: {}", currencyId, e); + if (conn != null) { try { conn.rollback(); } catch (SQLException ignored) {} } + } finally { + if (conn != null) { try { conn.close(); } catch (SQLException ignored) {} } + } + return false; + } + + public boolean renameCurrency(String oldName, String newName) { + String sql = String.format("UPDATE %s SET name = ? WHERE name = ?", tables.getCurrencies()); + + Connection conn = null; + try { + conn = dbManager.getConnection(); + + try (PreparedStatement stmt = conn.prepareStatement(sql)) { + stmt.setString(1, newName); + stmt.setString(2, oldName); + int rows = stmt.executeUpdate(); + if (rows == 0) { + conn.rollback(); + return false; + } + } + + try (PreparedStatement stmt = conn.prepareStatement("UPDATE cme_exchange_rates SET currency_name = ? WHERE currency_name = ?")) { + stmt.setString(1, newName); + stmt.setString(2, oldName); + stmt.executeUpdate(); + } + + conn.commit(); + CardinalCooperativeCapitalismMarketEconomy.LOGGER.info("Renamed currency: {} -> {}", oldName, newName); + return true; + } catch (SQLException e) { + CardinalCooperativeCapitalismMarketEconomy.LOGGER.error("Failed to rename currency: {}", oldName, e); + if (conn != null) { try { conn.rollback(); } catch (SQLException ignored) {} } + } finally { + if (conn != null) { try { conn.close(); } catch (SQLException ignored) {} } + } + return false; + } + + public boolean updateCurrency(String name, String newSymbol, Boolean newTradable) { + StringBuilder sqlBuilder = new StringBuilder(String.format("UPDATE %s SET ", tables.getCurrencies())); + boolean first = true; + + if (newSymbol != null) { + sqlBuilder.append("symbol = ?"); + first = false; + } + if (newTradable != null) { + if (!first) sqlBuilder.append(", "); + sqlBuilder.append("tradable = ?"); + } + + sqlBuilder.append(" WHERE name = ?"); + String sql = sqlBuilder.toString(); + + Connection conn = null; + try { + conn = dbManager.getConnection(); + try (PreparedStatement stmt = conn.prepareStatement(sql)) { + int paramIndex = 1; + if (newSymbol != null) { + stmt.setString(paramIndex++, newSymbol); + } + if (newTradable != null) { + stmt.setInt(paramIndex++, newTradable ? 1 : 0); + } + stmt.setString(paramIndex, name); + + int rows = stmt.executeUpdate(); + if (rows > 0) { + conn.commit(); + CardinalCooperativeCapitalismMarketEconomy.LOGGER.info("Updated currency: {}", name); + return true; + } + } + conn.rollback(); + } catch (SQLException e) { + CardinalCooperativeCapitalismMarketEconomy.LOGGER.error("Failed to update currency: {}", name, e); + if (conn != null) { try { conn.rollback(); } catch (SQLException ignored) {} } + } finally { + if (conn != null) { try { conn.close(); } catch (SQLException ignored) {} } + } + return false; + } + + public boolean setTradable(int currencyId, boolean tradable) { + String sql = String.format("UPDATE %s SET tradable = ? WHERE id = ?", tables.getCurrencies()); + Connection conn = null; + try { + conn = dbManager.getConnection(); + try (PreparedStatement stmt = conn.prepareStatement(sql)) { + stmt.setInt(1, tradable ? 1 : 0); + stmt.setInt(2, currencyId); + int rows = stmt.executeUpdate(); + if (rows > 0) { + conn.commit(); + CardinalCooperativeCapitalismMarketEconomy.LOGGER.info("Currency {} tradable set to {}", currencyId, tradable); + return true; + } + } + conn.rollback(); + } catch (SQLException e) { + CardinalCooperativeCapitalismMarketEconomy.LOGGER.error("Failed to set tradable for currency: {}", currencyId, e); + if (conn != null) { try { conn.rollback(); } catch (SQLException ignored) {} } + } finally { + if (conn != null) { try { conn.close(); } catch (SQLException ignored) {} } + } + return false; + } + + public Optional getCurrency(int currencyId) { + String sql = String.format("SELECT * FROM %s WHERE id = ?", tables.getCurrencies()); + Connection conn = null; + try { + conn = dbManager.getConnection(); + try (PreparedStatement stmt = conn.prepareStatement(sql)) { + stmt.setInt(1, currencyId); + try (ResultSet rs = stmt.executeQuery()) { + if (rs.next()) { + conn.commit(); + return Optional.of(mapCurrency(rs)); + } + } + } + conn.commit(); + } catch (SQLException e) { + CardinalCooperativeCapitalismMarketEconomy.LOGGER.error("Failed to get currency: {}", currencyId, e); + if (conn != null) { try { conn.rollback(); } catch (SQLException ignored) {} } + } finally { + if (conn != null) { try { conn.close(); } catch (SQLException ignored) {} } + } + return Optional.empty(); + } + + public Optional getCurrencyByName(String name) { + String sql = String.format("SELECT * FROM %s WHERE name = ?", tables.getCurrencies()); + Connection conn = null; + try { + conn = dbManager.getConnection(); + try (PreparedStatement stmt = conn.prepareStatement(sql)) { + stmt.setString(1, name); + try (ResultSet rs = stmt.executeQuery()) { + if (rs.next()) { + conn.commit(); + return Optional.of(mapCurrency(rs)); + } + } + } + conn.commit(); + } catch (SQLException e) { + CardinalCooperativeCapitalismMarketEconomy.LOGGER.error("Failed to get currency by name: {}", name, e); + if (conn != null) { try { conn.rollback(); } catch (SQLException ignored) {} } + } finally { + if (conn != null) { try { conn.close(); } catch (SQLException ignored) {} } + } + return Optional.empty(); + } + + public List getAllCurrencies() { + String sql = String.format("SELECT * FROM %s", tables.getCurrencies()); + List currencies = new ArrayList<>(); + Connection conn = null; + try { + conn = dbManager.getConnection(); + try (Statement stmt = conn.createStatement(); + ResultSet rs = stmt.executeQuery(sql)) { + while (rs.next()) { + currencies.add(mapCurrency(rs)); + } + } + conn.commit(); + } catch (SQLException e) { + CardinalCooperativeCapitalismMarketEconomy.LOGGER.error("Failed to get all currencies", e); + if (conn != null) { try { conn.rollback(); } catch (SQLException ignored) {} } + } finally { + if (conn != null) { try { conn.close(); } catch (SQLException ignored) {} } + } + return currencies; + } + + private Currency mapCurrency(ResultSet rs) throws SQLException { + Currency currency = new Currency(); + currency.setId(rs.getInt("id")); + currency.setName(rs.getString("name")); + currency.setSymbol(rs.getString("symbol")); + currency.setTradable(rs.getInt("tradable") == 1); + return currency; + } +} diff --git a/src/main/java/com/gvsds/tcme/manager/ExchangeRateManager.java b/src/main/java/com/gvsds/tcme/manager/ExchangeRateManager.java new file mode 100644 index 0000000..c58693d --- /dev/null +++ b/src/main/java/com/gvsds/tcme/manager/ExchangeRateManager.java @@ -0,0 +1,134 @@ +package com.gvsds.tcme.manager; + +import com.gvsds.tcme.CardinalCooperativeCapitalismMarketEconomy; +import com.gvsds.tcme.config.Config; +import com.gvsds.tcme.database.DatabaseManager; + +import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; + +public class ExchangeRateManager { + + private final DatabaseManager dbManager; + private final Config.Tables tables; + private final String exchangeRatesTable = "cme_exchange_rates"; + private final boolean isMySQL; + + public ExchangeRateManager(DatabaseManager dbManager, Config.Tables tables, boolean isMySQL) { + this.dbManager = dbManager; + this.tables = tables; + this.isMySQL = isMySQL; + initializeTable(); + } + + private void initializeTable() { + String textType = isMySQL ? "VARCHAR(255)" : "TEXT"; + String autoIncrement = isMySQL ? "AUTO_INCREMENT" : "AUTOINCREMENT"; + String engineClause = isMySQL ? "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4" : ""; + + String sql = String.format(""" + CREATE TABLE IF NOT EXISTS %s ( + id INTEGER PRIMARY KEY %s, + currency_name %s NOT NULL UNIQUE, + rate REAL NOT NULL DEFAULT 1.0, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ) %s + """, exchangeRatesTable, autoIncrement, textType, engineClause); + + Connection conn = null; + try { + conn = dbManager.getConnection(); + try (Statement stmt = conn.createStatement()) { + stmt.execute(sql); + } + conn.commit(); + } catch (SQLException e) { + CardinalCooperativeCapitalismMarketEconomy.LOGGER.error("Failed to create exchange rates table", e); + if (conn != null) { try { conn.rollback(); } catch (SQLException ignored) {} } + } finally { + if (conn != null) { try { conn.close(); } catch (SQLException ignored) {} } + } + } + + public void setRate(int currencyId, double rate) { + String sql; + if (isMySQL) { + sql = String.format( + "INSERT INTO %s (currency_name, rate, created_at, updated_at) VALUES (?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP) " + + "ON DUPLICATE KEY UPDATE rate = VALUES(rate), updated_at = CURRENT_TIMESTAMP", + exchangeRatesTable + ); + } else { + sql = String.format( + "INSERT INTO %s (currency_name, rate, created_at, updated_at) VALUES (?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP) " + + "ON CONFLICT(currency_name) DO UPDATE SET rate = ?, updated_at = CURRENT_TIMESTAMP", + exchangeRatesTable + ); + } + + Connection conn = null; + try { + conn = dbManager.getConnection(); + String nameSql = String.format("SELECT name FROM %s WHERE id = ?", tables.getCurrencies()); + try (java.sql.PreparedStatement nameStmt = conn.prepareStatement(nameSql)) { + nameStmt.setInt(1, currencyId); + try (ResultSet rs = nameStmt.executeQuery()) { + if (!rs.next()) { + conn.rollback(); + return; + } + String currencyName = rs.getString("name"); + + try (java.sql.PreparedStatement stmt = conn.prepareStatement(sql)) { + stmt.setString(1, currencyName); + stmt.setDouble(2, rate); + if (!isMySQL) { + stmt.setDouble(3, rate); + } + stmt.executeUpdate(); + } + } + } + conn.commit(); + } catch (SQLException e) { + CardinalCooperativeCapitalismMarketEconomy.LOGGER.error("Failed to set exchange rate for currency: {}", currencyId, e); + if (conn != null) { try { conn.rollback(); } catch (SQLException ignored) {} } + } finally { + if (conn != null) { try { conn.close(); } catch (SQLException ignored) {} } + } + } + + public double getRate(int currencyId) { + String sql = String.format("SELECT rate FROM %s WHERE currency_name = (SELECT name FROM %s WHERE id = ?)", exchangeRatesTable, tables.getCurrencies()); + Connection conn = null; + try { + conn = dbManager.getConnection(); + try (java.sql.PreparedStatement stmt = conn.prepareStatement(sql)) { + stmt.setInt(1, currencyId); + try (ResultSet rs = stmt.executeQuery()) { + if (rs.next()) { + double rate = rs.getDouble("rate"); + conn.commit(); + return rate; + } + } + } + conn.commit(); + } catch (SQLException e) { + CardinalCooperativeCapitalismMarketEconomy.LOGGER.error("Failed to get exchange rate for currency: {}", currencyId, e); + if (conn != null) { try { conn.rollback(); } catch (SQLException ignored) {} } + } finally { + if (conn != null) { try { conn.close(); } catch (SQLException ignored) {} } + } + return 1.0; + } + + public double convert(double amount, int fromCurrencyId, int toCurrencyId) { + double fromRate = getRate(fromCurrencyId); + double toRate = getRate(toCurrencyId); + return amount * (fromRate / toRate); + } +} diff --git a/src/main/java/com/gvsds/tcme/manager/PaymentRequestManager.java b/src/main/java/com/gvsds/tcme/manager/PaymentRequestManager.java new file mode 100644 index 0000000..6756f95 --- /dev/null +++ b/src/main/java/com/gvsds/tcme/manager/PaymentRequestManager.java @@ -0,0 +1,254 @@ +package com.gvsds.tcme.manager; + +import com.gvsds.tcme.CardinalCooperativeCapitalismMarketEconomy; +import com.gvsds.tcme.config.Config; +import com.gvsds.tcme.database.DatabaseManager; +import com.gvsds.tcme.model.PaymentRequest; + +import java.nio.ByteBuffer; +import java.sql.*; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +public class PaymentRequestManager { + + private final DatabaseManager dbManager; + private final Config.Tables tables; + private final boolean isMySQL; + + public PaymentRequestManager(DatabaseManager dbManager, Config.Tables tables, BalanceManager balanceManager, TransactionManager transactionManager, boolean isMySQL) { + this.dbManager = dbManager; + this.tables = tables; + this.isMySQL = isMySQL; + } + + public Optional createRequest(UUID fromUUID, UUID toUUID, int currencyId, double amount) { + if (amount <= 0) { + return Optional.empty(); + } + + String sql = String.format( + "INSERT INTO %s (from_uuid, to_uuid, currency_id, amount, status, created_at) VALUES (?, ?, ?, ?, 'pending', CURRENT_TIMESTAMP)", + tables.getPaymentRequests() + ); + + Connection conn = null; + try { + conn = dbManager.getConnection(); + try (PreparedStatement stmt = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) { + setUUID(stmt, 1, fromUUID); + setUUID(stmt, 2, toUUID); + stmt.setInt(3, currencyId); + stmt.setDouble(4, amount); + stmt.executeUpdate(); + + try (ResultSet rs = stmt.getGeneratedKeys()) { + if (rs.next()) { + PaymentRequest request = new PaymentRequest(fromUUID, toUUID, currencyId, amount); + request.setId(rs.getInt(1)); + conn.commit(); + return Optional.of(request); + } + } + } + conn.rollback(); + } catch (SQLException e) { + CardinalCooperativeCapitalismMarketEconomy.LOGGER.error("Failed to create payment request", e); + if (conn != null) { try { conn.rollback(); } catch (SQLException ignored) {} } + } finally { + if (conn != null) { try { conn.close(); } catch (SQLException ignored) {} } + } + return Optional.empty(); + } + + public boolean acceptRequest(int requestId) { + String selectSql = String.format("SELECT * FROM %s WHERE id = ? AND status = 'pending'", tables.getPaymentRequests()); + String updateSql = String.format("UPDATE %s SET status = 'completed' WHERE id = ?", tables.getPaymentRequests()); + String insertTransactionSql = String.format( + "INSERT INTO %s (from_uuid, to_uuid, currency_id, amount, type, description, created_at) VALUES (?, ?, ?, ?, 'apay_accept', ?, CURRENT_TIMESTAMP)", + tables.getTransactions() + ); + + Connection conn = null; + try { + conn = dbManager.getConnection(); + + UUID fromUUID, toUUID; + int currencyId; + double amount; + + try (PreparedStatement selectStmt = conn.prepareStatement(selectSql)) { + selectStmt.setInt(1, requestId); + try (ResultSet rs = selectStmt.executeQuery()) { + if (!rs.next()) { + conn.rollback(); + return false; + } + + fromUUID = getUUIDFromBytes(rs.getBytes("from_uuid")); + toUUID = getUUIDFromBytes(rs.getBytes("to_uuid")); + currencyId = rs.getInt("currency_id"); + amount = rs.getDouble("amount"); + } + } + + BalanceManager bm = CardinalCooperativeCapitalismMarketEconomy.getBalanceManager(); + double toBalance = bm.getBalance(conn, toUUID, currencyId); + if (toBalance < amount) { + conn.rollback(); + return false; + } + + bm.addBalance(conn, toUUID, currencyId, -amount); + bm.addBalance(conn, fromUUID, currencyId, amount); + + try (PreparedStatement stmt = conn.prepareStatement(insertTransactionSql)) { + setUUID(stmt, 1, toUUID); + setUUID(stmt, 2, fromUUID); + stmt.setInt(3, currencyId); + stmt.setDouble(4, amount); + stmt.setString(5, "Accepted payment request #" + requestId); + stmt.executeUpdate(); + } + + try (PreparedStatement updateStmt = conn.prepareStatement(updateSql)) { + updateStmt.setInt(1, requestId); + updateStmt.executeUpdate(); + } + + conn.commit(); + CardinalCooperativeCapitalismMarketEconomy.LOGGER.info("Payment request #{} accepted", requestId); + return true; + } catch (SQLException e) { + CardinalCooperativeCapitalismMarketEconomy.LOGGER.error("Failed to accept payment request", e); + if (conn != null) { try { conn.rollback(); } catch (SQLException ignored) {} } + } finally { + if (conn != null) { try { conn.close(); } catch (SQLException ignored) {} } + } + return false; + } + + public boolean cancelRequest(int requestId) { + String sql = String.format("UPDATE %s SET status = 'cancelled' WHERE id = ? AND status = 'pending'", tables.getPaymentRequests()); + Connection conn = null; + try { + conn = dbManager.getConnection(); + try (PreparedStatement stmt = conn.prepareStatement(sql)) { + stmt.setInt(1, requestId); + int rows = stmt.executeUpdate(); + if (rows > 0) { + conn.commit(); + return true; + } + } + conn.rollback(); + } catch (SQLException e) { + CardinalCooperativeCapitalismMarketEconomy.LOGGER.error("Failed to cancel payment request", e); + if (conn != null) { try { conn.rollback(); } catch (SQLException ignored) {} } + } finally { + if (conn != null) { try { conn.close(); } catch (SQLException ignored) {} } + } + return false; + } + + public boolean rejectRequest(int requestId) { + String sql = String.format("UPDATE %s SET status = 'rejected' WHERE id = ? AND status = 'pending'", tables.getPaymentRequests()); + Connection conn = null; + try { + conn = dbManager.getConnection(); + try (PreparedStatement stmt = conn.prepareStatement(sql)) { + stmt.setInt(1, requestId); + int rows = stmt.executeUpdate(); + if (rows > 0) { + conn.commit(); + return true; + } + } + conn.rollback(); + } catch (SQLException e) { + CardinalCooperativeCapitalismMarketEconomy.LOGGER.error("Failed to reject payment request", e); + if (conn != null) { try { conn.rollback(); } catch (SQLException ignored) {} } + } finally { + if (conn != null) { try { conn.close(); } catch (SQLException ignored) {} } + } + return false; + } + + public Optional getRequestById(int requestId) { + String sql = String.format("SELECT * FROM %s WHERE id = ?", tables.getPaymentRequests()); + Connection conn = null; + try { + conn = dbManager.getConnection(); + try (PreparedStatement stmt = conn.prepareStatement(sql)) { + stmt.setInt(1, requestId); + try (ResultSet rs = stmt.executeQuery()) { + if (rs.next()) { + return Optional.of(mapPaymentRequest(rs)); + } + } + } + conn.commit(); + } catch (SQLException e) { + CardinalCooperativeCapitalismMarketEconomy.LOGGER.error("Failed to get payment request by id", e); + if (conn != null) { try { conn.rollback(); } catch (SQLException ignored) {} } + } finally { + if (conn != null) { try { conn.close(); } catch (SQLException ignored) {} } + } + return Optional.empty(); + } + + public List getPendingRequests(UUID playerUUID) { + String sql = String.format("SELECT * FROM %s WHERE to_uuid = ? AND status = 'pending'", tables.getPaymentRequests()); + List requests = new ArrayList<>(); + Connection conn = null; + try { + conn = dbManager.getConnection(); + try (PreparedStatement stmt = conn.prepareStatement(sql)) { + setUUID(stmt, 1, playerUUID); + try (ResultSet rs = stmt.executeQuery()) { + while (rs.next()) { + requests.add(mapPaymentRequest(rs)); + } + } + } + conn.commit(); + } catch (SQLException e) { + CardinalCooperativeCapitalismMarketEconomy.LOGGER.error("Failed to get pending requests", e); + if (conn != null) { try { conn.rollback(); } catch (SQLException ignored) {} } + } finally { + if (conn != null) { try { conn.close(); } catch (SQLException ignored) {} } + } + return requests; + } + + private PaymentRequest mapPaymentRequest(ResultSet rs) throws SQLException { + PaymentRequest request = new PaymentRequest(); + request.setId(rs.getInt("id")); + request.setFromUUID(getUUIDFromBytes(rs.getBytes("from_uuid"))); + request.setToUUID(getUUIDFromBytes(rs.getBytes("to_uuid"))); + request.setCurrencyId(rs.getInt("currency_id")); + request.setAmount(rs.getDouble("amount")); + request.setStatus(rs.getString("status")); + request.setCreatedAt(rs.getTimestamp("created_at")); + return request; + } + + private void setUUID(PreparedStatement stmt, int index, UUID uuid) throws SQLException { + ByteBuffer buffer = ByteBuffer.wrap(new byte[16]); + buffer.putLong(uuid.getMostSignificantBits()); + buffer.putLong(uuid.getLeastSignificantBits()); + stmt.setBytes(index, buffer.array()); + } + + private UUID getUUIDFromBytes(byte[] bytes) { + if (bytes == null || bytes.length != 16) { + return null; + } + ByteBuffer buffer = ByteBuffer.wrap(bytes); + long mostSigBits = buffer.getLong(); + long leastSigBits = buffer.getLong(); + return new UUID(mostSigBits, leastSigBits); + } +} diff --git a/src/main/java/com/gvsds/tcme/manager/PlayerSettingsManager.java b/src/main/java/com/gvsds/tcme/manager/PlayerSettingsManager.java new file mode 100644 index 0000000..d7a411d --- /dev/null +++ b/src/main/java/com/gvsds/tcme/manager/PlayerSettingsManager.java @@ -0,0 +1,125 @@ +package com.gvsds.tcme.manager; + +import com.gvsds.tcme.CardinalCooperativeCapitalismMarketEconomy; +import com.gvsds.tcme.config.Config; +import com.gvsds.tcme.database.DatabaseManager; + +import java.nio.ByteBuffer; +import java.sql.*; +import java.util.Optional; +import java.util.UUID; + +public class PlayerSettingsManager { + + private final DatabaseManager dbManager; + private final Config.Tables tables; + private final boolean isMySQL; + private final String settingsTable; + + public PlayerSettingsManager(DatabaseManager dbManager, Config.Tables tables, boolean isMySQL) { + this.dbManager = dbManager; + this.tables = tables; + this.isMySQL = isMySQL; + this.settingsTable = "cme_player_settings"; + initializeTable(); + } + + private void initializeTable() { + String uuidType = isMySQL ? "BINARY(16)" : "BLOB"; + String textType = isMySQL ? "VARCHAR(50)" : "TEXT"; + String autoIncrement = isMySQL ? "AUTO_INCREMENT" : "AUTOINCREMENT"; + String engineClause = isMySQL ? "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4" : ""; + + String sql = String.format(""" + CREATE TABLE IF NOT EXISTS %s ( + id INTEGER PRIMARY KEY %s, + user_uuid %s NOT NULL UNIQUE, + language %s DEFAULT 'en_us', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ) %s + """, settingsTable, autoIncrement, uuidType, textType, engineClause); + + Connection conn = null; + try { + conn = dbManager.getConnection(); + try (Statement stmt = conn.createStatement()) { + stmt.execute(sql); + } + conn.commit(); + } catch (SQLException e) { + CardinalCooperativeCapitalismMarketEconomy.LOGGER.error("Failed to create player settings table", e); + if (conn != null) { try { conn.rollback(); } catch (SQLException ignored) {} } + } finally { + if (conn != null) { try { conn.close(); } catch (SQLException ignored) {} } + } + } + + public void setLanguage(UUID playerUUID, String language) { + String sql; + if (isMySQL) { + sql = String.format( + "INSERT INTO %s (user_uuid, language, created_at, updated_at) VALUES (?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP) " + + "ON DUPLICATE KEY UPDATE language = VALUES(language), updated_at = CURRENT_TIMESTAMP", + settingsTable + ); + } else { + sql = String.format( + "INSERT INTO %s (user_uuid, language, created_at, updated_at) VALUES (?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP) " + + "ON CONFLICT(user_uuid) DO UPDATE SET language = ?, updated_at = CURRENT_TIMESTAMP", + settingsTable + ); + } + + Connection conn = null; + try { + conn = dbManager.getConnection(); + try (PreparedStatement stmt = conn.prepareStatement(sql)) { + setUUID(stmt, 1, playerUUID); + stmt.setString(2, language); + if (!isMySQL) { + stmt.setString(3, language); + } + stmt.executeUpdate(); + } + conn.commit(); + } catch (SQLException e) { + CardinalCooperativeCapitalismMarketEconomy.LOGGER.error("Failed to set language for player: {}", playerUUID, e); + if (conn != null) { try { conn.rollback(); } catch (SQLException ignored) {} } + } finally { + if (conn != null) { try { conn.close(); } catch (SQLException ignored) {} } + } + } + + public Optional getLanguage(UUID playerUUID) { + String sql = String.format("SELECT language FROM %s WHERE user_uuid = ?", settingsTable); + Connection conn = null; + try { + conn = dbManager.getConnection(); + try (PreparedStatement stmt = conn.prepareStatement(sql)) { + setUUID(stmt, 1, playerUUID); + try (ResultSet rs = stmt.executeQuery()) { + if (rs.next()) { + String lang = rs.getString("language"); + conn.commit(); + return Optional.of(lang); + } + } + } + conn.commit(); + } catch (SQLException e) { + CardinalCooperativeCapitalismMarketEconomy.LOGGER.error("Failed to get language for player: {}", playerUUID, e); + if (conn != null) { try { conn.rollback(); } catch (SQLException ignored) {} } + } finally { + if (conn != null) { try { conn.close(); } catch (SQLException ignored) {} } + } + return Optional.empty(); + } + + private void setUUID(PreparedStatement stmt, int index, UUID uuid) throws SQLException { + ByteBuffer buffer = ByteBuffer.wrap(new byte[16]); + buffer.putLong(uuid.getMostSignificantBits()); + buffer.putLong(uuid.getLeastSignificantBits()); + stmt.setBytes(index, buffer.array()); + } +} diff --git a/src/main/java/com/gvsds/tcme/manager/TransactionManager.java b/src/main/java/com/gvsds/tcme/manager/TransactionManager.java new file mode 100644 index 0000000..9aaf56e --- /dev/null +++ b/src/main/java/com/gvsds/tcme/manager/TransactionManager.java @@ -0,0 +1,141 @@ +package com.gvsds.tcme.manager; + +import com.gvsds.tcme.CardinalCooperativeCapitalismMarketEconomy; +import com.gvsds.tcme.config.Config; +import com.gvsds.tcme.database.DatabaseManager; + +import java.nio.ByteBuffer; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.UUID; + +public class TransactionManager { + + private final DatabaseManager dbManager; + private final Config.Tables tables; + private final boolean isMySQL; + + public TransactionManager(DatabaseManager dbManager, Config.Tables tables, BalanceManager balanceManager, boolean isMySQL) { + this.dbManager = dbManager; + this.tables = tables; + this.isMySQL = isMySQL; + } + + public boolean transfer(UUID fromUUID, UUID toUUID, int currencyId, double amount, String type, String description) { + if (amount <= 0) { + return false; + } + + String sql = String.format( + "INSERT INTO %s (from_uuid, to_uuid, currency_id, amount, type, description, created_at) VALUES (?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)", + tables.getTransactions() + ); + + String checkBalanceSql = String.format("SELECT balance FROM %s WHERE user_uuid = ? AND currency_id = ?", tables.getBalances()); + String updateBalanceSql; + if (isMySQL) { + updateBalanceSql = String.format( + "INSERT INTO %s (user_uuid, currency_id, balance, created_at, updated_at) VALUES (?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP) " + + "ON DUPLICATE KEY UPDATE balance = VALUES(balance), updated_at = CURRENT_TIMESTAMP", + tables.getBalances() + ); + } else { + updateBalanceSql = String.format( + "INSERT INTO %s (user_uuid, currency_id, balance, created_at, updated_at) VALUES (?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP) " + + "ON CONFLICT(user_uuid, currency_id) DO UPDATE SET balance = excluded.balance, updated_at = CURRENT_TIMESTAMP", + tables.getBalances() + ); + } + + Connection conn = null; + try { + conn = dbManager.getConnection(); + + double fromBalance; + try (PreparedStatement stmt = conn.prepareStatement(checkBalanceSql)) { + setUUID(stmt, 1, fromUUID); + stmt.setInt(2, currencyId); + try (ResultSet rs = stmt.executeQuery()) { + if (rs.next()) { + fromBalance = rs.getDouble("balance"); + } else { + fromBalance = 0.0; + } + } + } + + if (fromBalance < amount) { + conn.rollback(); + return false; + } + + try (PreparedStatement stmt = conn.prepareStatement(updateBalanceSql)) { + setUUID(stmt, 1, fromUUID); + stmt.setInt(2, currencyId); + stmt.setDouble(3, fromBalance - amount); + stmt.executeUpdate(); + } + + double toBalance; + try (PreparedStatement stmt = conn.prepareStatement(checkBalanceSql)) { + setUUID(stmt, 1, toUUID); + stmt.setInt(2, currencyId); + try (ResultSet rs = stmt.executeQuery()) { + if (rs.next()) { + toBalance = rs.getDouble("balance"); + } else { + toBalance = 0.0; + } + } + } + + try (PreparedStatement stmt = conn.prepareStatement(updateBalanceSql)) { + setUUID(stmt, 1, toUUID); + stmt.setInt(2, currencyId); + stmt.setDouble(3, toBalance + amount); + stmt.executeUpdate(); + } + + try (PreparedStatement stmt = conn.prepareStatement(sql)) { + setUUID(stmt, 1, fromUUID); + setUUID(stmt, 2, toUUID); + stmt.setInt(3, currencyId); + stmt.setDouble(4, amount); + stmt.setString(5, type); + stmt.setString(6, description); + stmt.executeUpdate(); + } + + conn.commit(); + CardinalCooperativeCapitalismMarketEconomy.LOGGER.info("Transfer: {} -> {} amount: {} currency: {}", fromUUID, toUUID, amount, currencyId); + return true; + } catch (SQLException e) { + CardinalCooperativeCapitalismMarketEconomy.LOGGER.error("Failed to process transfer", e); + if (conn != null) { + try { + conn.rollback(); + } catch (SQLException re) { + CardinalCooperativeCapitalismMarketEconomy.LOGGER.error("Failed to rollback transfer", re); + } + } + } finally { + if (conn != null) { + try { + conn.close(); + } catch (SQLException ce) { + CardinalCooperativeCapitalismMarketEconomy.LOGGER.error("Failed to close connection", ce); + } + } + } + return false; + } + + private void setUUID(PreparedStatement stmt, int index, UUID uuid) throws SQLException { + ByteBuffer buffer = ByteBuffer.wrap(new byte[16]); + buffer.putLong(uuid.getMostSignificantBits()); + buffer.putLong(uuid.getLeastSignificantBits()); + stmt.setBytes(index, buffer.array()); + } +} diff --git a/src/main/java/com/gvsds/tcme/model/Currency.java b/src/main/java/com/gvsds/tcme/model/Currency.java new file mode 100644 index 0000000..abdedb2 --- /dev/null +++ b/src/main/java/com/gvsds/tcme/model/Currency.java @@ -0,0 +1,30 @@ +package com.gvsds.tcme.model; + +import java.util.UUID; + +public class Currency { + private int id; + private String name; + private String symbol; + private boolean tradable; + + public Currency() {} + + public Currency(String name, String symbol, boolean tradable) { + this.name = name; + this.symbol = symbol; + this.tradable = tradable; + } + + public int getId() { return id; } + public void setId(int id) { this.id = id; } + + public String getName() { return name; } + public void setName(String name) { this.name = name; } + + public String getSymbol() { return symbol; } + public void setSymbol(String symbol) { this.symbol = symbol; } + + public boolean isTradable() { return tradable; } + public void setTradable(boolean tradable) { this.tradable = tradable; } +} diff --git a/src/main/java/com/gvsds/tcme/model/PaymentRequest.java b/src/main/java/com/gvsds/tcme/model/PaymentRequest.java new file mode 100644 index 0000000..c655d93 --- /dev/null +++ b/src/main/java/com/gvsds/tcme/model/PaymentRequest.java @@ -0,0 +1,45 @@ +package com.gvsds.tcme.model; + +import java.sql.Timestamp; +import java.util.UUID; + +public class PaymentRequest { + private int id; + private UUID fromUUID; + private UUID toUUID; + private int currencyId; + private double amount; + private String status; + private Timestamp createdAt; + + public PaymentRequest() {} + + public PaymentRequest(UUID fromUUID, UUID toUUID, int currencyId, double amount) { + this.fromUUID = fromUUID; + this.toUUID = toUUID; + this.currencyId = currencyId; + this.amount = amount; + this.status = "pending"; + } + + public int getId() { return id; } + public void setId(int id) { this.id = id; } + + public UUID getFromUUID() { return fromUUID; } + public void setFromUUID(UUID fromUUID) { this.fromUUID = fromUUID; } + + public UUID getToUUID() { return toUUID; } + public void setToUUID(UUID toUUID) { this.toUUID = toUUID; } + + public int getCurrencyId() { return currencyId; } + public void setCurrencyId(int currencyId) { this.currencyId = currencyId; } + + public double getAmount() { return amount; } + public void setAmount(double amount) { this.amount = amount; } + + public String getStatus() { return status; } + public void setStatus(String status) { this.status = status; } + + public Timestamp getCreatedAt() { return createdAt; } + public void setCreatedAt(Timestamp createdAt) { this.createdAt = createdAt; } +} diff --git a/src/main/java/com/gvsds/tcme/model/PlayerBalance.java b/src/main/java/com/gvsds/tcme/model/PlayerBalance.java new file mode 100644 index 0000000..95e2ddb --- /dev/null +++ b/src/main/java/com/gvsds/tcme/model/PlayerBalance.java @@ -0,0 +1,30 @@ +package com.gvsds.tcme.model; + +import java.util.UUID; + +public class PlayerBalance { + private int id; + private UUID userUUID; + private int currencyId; + private double balance; + + public PlayerBalance() {} + + public PlayerBalance(UUID userUUID, int currencyId, double balance) { + this.userUUID = userUUID; + this.currencyId = currencyId; + this.balance = balance; + } + + public int getId() { return id; } + public void setId(int id) { this.id = id; } + + public UUID getUserUUID() { return userUUID; } + public void setUserUUID(UUID userUUID) { this.userUUID = userUUID; } + + public int getCurrencyId() { return currencyId; } + public void setCurrencyId(int currencyId) { this.currencyId = currencyId; } + + public double getBalance() { return balance; } + public void setBalance(double balance) { this.balance = balance; } +} diff --git a/src/main/java/com/gvsds/tcme/model/Transaction.java b/src/main/java/com/gvsds/tcme/model/Transaction.java new file mode 100644 index 0000000..8da8050 --- /dev/null +++ b/src/main/java/com/gvsds/tcme/model/Transaction.java @@ -0,0 +1,50 @@ +package com.gvsds.tcme.model; + +import java.sql.Timestamp; +import java.util.UUID; + +public class Transaction { + private int id; + private UUID fromUUID; + private UUID toUUID; + private int currencyId; + private double amount; + private String type; + private String description; + private Timestamp createdAt; + + public Transaction() {} + + public Transaction(UUID fromUUID, UUID toUUID, int currencyId, double amount, String type, String description) { + this.fromUUID = fromUUID; + this.toUUID = toUUID; + this.currencyId = currencyId; + this.amount = amount; + this.type = type; + this.description = description; + } + + public int getId() { return id; } + public void setId(int id) { this.id = id; } + + public UUID getFromUUID() { return fromUUID; } + public void setFromUUID(UUID fromUUID) { this.fromUUID = fromUUID; } + + public UUID getToUUID() { return toUUID; } + public void setToUUID(UUID toUUID) { this.toUUID = toUUID; } + + public int getCurrencyId() { return currencyId; } + public void setCurrencyId(int currencyId) { this.currencyId = currencyId; } + + public double getAmount() { return amount; } + public void setAmount(double amount) { this.amount = amount; } + + public String getType() { return type; } + public void setType(String type) { this.type = type; } + + public String getDescription() { return description; } + public void setDescription(String description) { this.description = description; } + + public Timestamp getCreatedAt() { return createdAt; } + public void setCreatedAt(Timestamp createdAt) { this.createdAt = createdAt; } +} diff --git a/src/main/java/com/gvsds/tcme/util/TextUtil.java b/src/main/java/com/gvsds/tcme/util/TextUtil.java new file mode 100644 index 0000000..e36558b --- /dev/null +++ b/src/main/java/com/gvsds/tcme/util/TextUtil.java @@ -0,0 +1,80 @@ +package com.gvsds.tcme.util; + +import net.minecraft.text.MutableText; +import net.minecraft.text.Text; +import net.minecraft.util.Formatting; + +import java.util.*; + +public class TextUtil { + + private static final Map CODE_MAP = new HashMap<>(); + + static { + CODE_MAP.put('0', Formatting.BLACK); + CODE_MAP.put('1', Formatting.DARK_BLUE); + CODE_MAP.put('2', Formatting.DARK_GREEN); + CODE_MAP.put('3', Formatting.DARK_AQUA); + CODE_MAP.put('4', Formatting.DARK_RED); + CODE_MAP.put('5', Formatting.DARK_PURPLE); + CODE_MAP.put('6', Formatting.GOLD); + CODE_MAP.put('7', Formatting.GRAY); + CODE_MAP.put('8', Formatting.DARK_GRAY); + CODE_MAP.put('9', Formatting.BLUE); + CODE_MAP.put('a', Formatting.GREEN); + CODE_MAP.put('b', Formatting.AQUA); + CODE_MAP.put('c', Formatting.RED); + CODE_MAP.put('d', Formatting.LIGHT_PURPLE); + CODE_MAP.put('e', Formatting.YELLOW); + CODE_MAP.put('f', Formatting.WHITE); + CODE_MAP.put('k', Formatting.OBFUSCATED); + CODE_MAP.put('l', Formatting.BOLD); + CODE_MAP.put('m', Formatting.STRIKETHROUGH); + CODE_MAP.put('n', Formatting.UNDERLINE); + CODE_MAP.put('o', Formatting.ITALIC); + } + + public static Text parseFormatting(String text) { + if (text == null || text.isEmpty()) return Text.empty(); + + MutableText result = null; + StringBuilder currentText = new StringBuilder(); + Set currentFormats = new LinkedHashSet<>(); + + for (int i = 0; i < text.length(); i++) { + if (text.charAt(i) == '\u00A7' && i + 1 < text.length()) { + if (currentText.length() > 0) { + MutableText part = Text.literal(currentText.toString()); + if (!currentFormats.isEmpty()) { + part.formatted(currentFormats.toArray(new Formatting[0])); + } + result = result == null ? part : result.append(part); + currentText = new StringBuilder(); + } + + char code = Character.toLowerCase(text.charAt(i + 1)); + if (code == 'r') { + currentFormats.clear(); + } else { + Formatting fmt = CODE_MAP.get(code); + if (fmt != null) { + currentFormats.add(fmt); + } + } + i++; + } else { + currentText.append(text.charAt(i)); + } + } + + if (currentText.length() > 0) { + MutableText part = Text.literal(currentText.toString()); + if (!currentFormats.isEmpty()) { + part.formatted(currentFormats.toArray(new Formatting[0])); + } + result = result == null ? part : result.append(part); + } + + return result != null ? result : Text.empty(); + } +} diff --git a/src/main/resources/assets/cardinal-cooperative-capitalism-market-economy/default_config.json b/src/main/resources/assets/cardinal-cooperative-capitalism-market-economy/default_config.json new file mode 100644 index 0000000..9a7eea6 --- /dev/null +++ b/src/main/resources/assets/cardinal-cooperative-capitalism-market-economy/default_config.json @@ -0,0 +1,12 @@ +{ + "database": { + "type": "sqlite", + "sqlitePath": "./config/cardinal-economy/database", + "mysqlHost": "localhost", + "mysqlPort": 3306, + "mysqlDatabase": "cardinal_economy", + "mysqlUsername": "root", + "mysqlPassword": "", + "useSSL": false + } +} diff --git a/src/main/resources/assets/cardinal-cooperative-capitalism-market-economy/icon.png b/src/main/resources/assets/cardinal-cooperative-capitalism-market-economy/icon.png new file mode 100644 index 0000000..699f52d Binary files /dev/null and b/src/main/resources/assets/cardinal-cooperative-capitalism-market-economy/icon.png differ diff --git a/src/main/resources/cardinal-cooperative-capitalism-market-economy.mixins.json b/src/main/resources/cardinal-cooperative-capitalism-market-economy.mixins.json new file mode 100644 index 0000000..c73d1b3 --- /dev/null +++ b/src/main/resources/cardinal-cooperative-capitalism-market-economy.mixins.json @@ -0,0 +1,12 @@ +{ + "required": true, + "package": "com.gvsds.tcme.mixin", + "compatibilityLevel": "JAVA_21", + "mixins": [], + "injectors": { + "defaultRequire": 1 + }, + "overwrites": { + "requireAnnotations": true + } +} diff --git a/src/main/resources/fabric.mod.json b/src/main/resources/fabric.mod.json new file mode 100644 index 0000000..44ef596 --- /dev/null +++ b/src/main/resources/fabric.mod.json @@ -0,0 +1,31 @@ +{ + "schemaVersion": 1, + "id": "cardinal-cooperative-capitalism-market-economy", + "version": "${version}", + "name": "Cardinal Cooperative Capitalism: Market Economy", + "description": "A server-side economy mod with SQLite/MySQL database support", + "authors": [ + "Me!" + ], + "contact": { + "homepage": "https://fabricmc.net/", + "sources": "https://github.com/FabricMC/fabric-example-mod" + }, + "license": "CC0-1.0", + "icon": "assets/cardinal-cooperative-capitalism-market-economy/icon.png", + "environment": "*", + "entrypoints": { + "main": [ + "com.gvsds.tcme.CardinalCooperativeCapitalismMarketEconomy" + ] + }, + "mixins": [ + "cardinal-cooperative-capitalism-market-economy.mixins.json" + ], + "depends": { + "fabricloader": ">=0.19.2", + "minecraft": "~1.21.10", + "java": ">=21", + "fabric-api": "*" + } +} \ No newline at end of file