Compare commits

...

58 Commits

Author SHA1 Message Date
mlus
1daf4ac1f5
Merge pull request #142 from mlus-asuka/backport-141-to-1.20.4
[Backport 1.20.4] Fixes #140
2025-11-22 22:41:49 +08:00
Fugit-5414
8ddfa3c86f Fixes #140
(cherry picked from commit 176d123f4e)
2025-11-22 14:37:45 +00:00
mlus
d64de0bdb1
Merge pull request #127 from mlus-asuka/1.20.4-dev
Mannualy Bump for 1.20.4
2025-10-14 16:55:03 +08:00
mlus
89b48fc3ff
Merge branch '1.20.4' into 1.20.4-dev 2025-10-14 16:51:05 +08:00
mlus
29e6f302eb line 155 make method return so curios data won't save at the first time 2025-10-14 16:18:41 +08:00
mlus
73d426f266 heartbeat 2025-10-14 16:18:38 +08:00
mlus
b0850d8922 chat sync reconnect system 2025-10-14 16:14:42 +08:00
dependabot[bot]
8b44dc7425 Bump gradle/actions from 4 to 5
Bumps [gradle/actions](https://github.com/gradle/actions) from 4 to 5.
- [Release notes](https://github.com/gradle/actions/releases)
- [Commits](https://github.com/gradle/actions/compare/v4...v5)

---
updated-dependencies:
- dependency-name: gradle/actions
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-14 16:12:36 +08:00
mlus
4c79dda284
Merge pull request #118 from mlus-asuka/backport-117-to-1.20.4
[Backport 1.20.4] may fix #111
2025-09-16 22:19:26 +08:00
mlus
cbcdc0e749 may fix #111
(cherry picked from commit befa36a303)
2025-09-16 14:14:39 +00:00
mlus
468b3182c3
Merge pull request #107 from EoD/make-configs-final-1.20.4
[1.20.4] make all config variables final
2025-08-01 11:54:00 +08:00
EoD
02de6b50d2 make all config variables final
the classes never change, even if the config is modified. Only the
values we read with get() inside the classes change.
2025-07-31 23:59:14 +00:00
mlus
c8a9792647
Merge pull request #106 from EoD/add-data_version-1.20.4
[1.20.4] add new data_version column to server_info table
2025-07-31 12:30:29 +08:00
EoD
b2f3aa5497 add new data_version column to server_info table 2025-07-30 23:28:34 +00:00
mlus
3d2eb77db1
Merge pull request #104 from mlus-asuka/backport-103-to-1.20.4
[Backport 1.20.4] Some SQL-related improvements
2025-07-30 12:45:18 +08:00
EoD
872168c88e extract addColumnIfNotExists into separate method
(cherry picked from commit 4fe13bd24d)
2025-07-30 04:03:06 +00:00
EoD
41732cbcd1 reformat insert into server_info for readability
Use the new format capabilities on SQL queries to make the insert more
readable.

(cherry picked from commit 7f06aa7511)
2025-07-30 04:03:06 +00:00
EoD
c434f3c6e4 allow format strings within SQL queries
This makes SQL queries more readable in some cases

(cherry picked from commit e1ac7adb11)
2025-07-30 04:03:06 +00:00
EoD
6ddd776468 clarify executeUpdate variants with and without db
(cherry picked from commit 8f77a96544)
2025-07-30 04:03:06 +00:00
mlus
9880c7e74c
Merge pull request #102 from mlus-asuka/backport-99-to-1.20.4
[Backport 1.20.4] make QueryResult AutoClosable
2025-07-29 18:40:02 +08:00
mlus
0b750ab852
Merge pull request #101 from mlus-asuka/backport-98-to-1.20.4
[Backport 1.20.4] remove duplicate UPDATE server_info
2025-07-29 18:37:28 +08:00
mlus
4eba9ad1d7
Merge pull request #100 from mlus-asuka/backport-97-to-1.20.4
[Backport 1.20.4] log the advancement's data type before altering
2025-07-29 18:36:05 +08:00
EoD
2c3512da8a make QueryResult AutoClosable
This allows QueryResults to be used within a try() block without
explicitely closing them.

(cherry picked from commit ad76e0e311)
2025-07-29 10:35:56 +00:00
EoD
3d111125a9 remove duplicate UPDATE server_info
the UPDATE is already happening in in the INSERT statement above

(cherry picked from commit 0e96107416)
2025-07-29 10:34:05 +00:00
EoD
0b4a6aa78a log the advancement's data type before altering
it seems that sometimes this is triggered on an existing database. This
should help identifying what is happening in these cases.

(cherry picked from commit 54cbb9c9a8)
2025-07-29 10:29:43 +00:00
mlus
675bf7e486
Merge pull request #95 from mlus-asuka/backport-93-to-1.20.4
[Backport 1.20.4] fix string to map generation with base64 encoding
2025-07-13 02:12:32 +08:00
EoD
9b7ebebf98 simplify and exit early in stringToGenericMap
(cherry picked from commit 53bdfe2309)
2025-07-12 05:14:33 +00:00
EoD
85f953a220 unify both stringToMap functions
(cherry picked from commit 228b835c2a)
2025-07-12 05:14:33 +00:00
EoD
db4af71215 fix string to map generation with base64 encoding
The new base64 encoding uses "=" characters as part of its encoding.
The previous code split those and trimmed them afterwards, making the
base64 not consistent with the rest of the code that assumed "=" are
still present, like "B64:e30=".

(cherry picked from commit de324a23be)
2025-07-12 05:14:33 +00:00
mlus
d657c7a819
Merge pull request #94 from mlus-asuka/backport-90-to-1.20.4
[Backport 1.20.4] unify item creation in curios and normal inventory
2025-07-11 12:48:26 +08:00
mlus
cc4e0cbbf9
Merge pull request #92 from EoD/remove-unnecessary-import-1.20.4
[1.20.4] remove unnecessary deserializeString import
2025-07-11 12:37:47 +08:00
EoD
6adb8c2622 unify item creation in curios and normal inventory
this also allows creation of placeholders within curios containers

(cherry picked from commit a70605a8b6)
2025-07-11 04:37:16 +00:00
EoD
a0f71ac6e5 remove unnecessary deserializeString import 2025-07-10 19:23:29 +00:00
mlus
3651c351cd
Merge pull request #89 from EoD/upgrade-items-between-versions-1.20.4
[1.20.4] properly upgrade items with newer MC versions
2025-07-07 00:08:07 +08:00
EoD
0e37cdc091 properly upgrade items with newer MC versions 2025-07-06 16:02:02 +00:00
mlus
ca2ef85dda
Merge pull request #86 from EoD/organize-imports-1.20.4
[1.20.4] optimize all imports
2025-06-18 12:28:50 +08:00
EoD
d69df0fa49 add vscode setting for IntelliJ-like imports 2025-06-17 23:52:11 +00:00
EoD
205d74e522 optimize all imports 2025-06-17 23:52:04 +00:00
mlus
02db84f9ce
Merge pull request #80 from mlus-asuka/1.20.4-dev
Fix error when no player in server
2025-06-06 13:14:58 +08:00
mlus
79e0e2cbe4 Fix error when no player in server 2025-06-06 13:09:32 +08:00
mlus
4bdad9f625
Merge pull request #78 from mlus-asuka/1.20.4-dev
Full tested ChatSync Feature
2025-06-05 13:18:23 +08:00
mlus
abae66a366
Merge branch '1.20.4' into 1.20.4-dev 2025-06-05 13:14:31 +08:00
mlus
0562b01138 Full tested ChatSync Feature 2025-06-05 12:43:41 +08:00
mlus
f8ea4039f6
Merge pull request #76 from mlus-asuka/backport-75-to-1.20.4
[Backport 1.20.4] bump mod version to 2.1.1
2025-05-19 13:12:50 +08:00
EoD
357579abbb bump mod version to 2.1.1
(cherry picked from commit e2b90ccf98)
2025-05-19 05:08:58 +00:00
mlus
54ec918259
Merge pull request #72 from mlus-asuka/backport-70-to-1.20.4
[Backport 1.20.4] fix XP being lost or duplicated
2025-05-19 01:38:34 +08:00
mlus
286bd107d9
Merge pull request #74 from mlus-asuka/backport-73-to-1.20.4
[Backport 1.20.4] use PAT in backport action
2025-05-19 01:38:06 +08:00
EoD
0203647818 use PAT in backport action
(cherry picked from commit e0f0b51851)
2025-05-18 17:32:38 +00:00
mlus
81b3914d3a
Merge pull request #71 from mlus-asuka/backport-67-to-1.20.4
[Backport 1.20.4] add action to automatically backport PRs
2025-05-19 01:27:31 +08:00
EoD
0bcdc86d32 fix XP being lost or duplicated
The current calculation did not work for larger amounts of levels
and either removed or added levels unintentionally.

(cherry picked from commit d83bad5a33)
2025-05-18 04:18:19 +00:00
EoD
2b835996c0 add action to automatically backport PRs
(cherry picked from commit 5ba7cc2972)
2025-05-18 04:16:59 +00:00
mlus
fe86792731
Merge pull request #69 from EoD/fix-language-files-1.20.4
[Backport 1.20.4] language file fix + remove unused override in the language files
2025-05-18 12:16:27 +08:00
EoD
26421096ac remove unused override in the language files 2025-05-17 19:21:58 +00:00
mlus
cf43f35ee9 language file fix 2025-05-17 19:21:58 +00:00
mlus
74451eecff
Merge pull request #65 from EoD/1.20.4-neoforge
Add support for 1.20.4 using NeoForge 20.4.248
2025-05-05 02:53:30 +08:00
EoD
22b628bdcb fix sophisticated backpack integration for 1.20.4 2025-05-04 18:43:11 +00:00
EoD
9eb8bdc4a0 add support for Minecraft 1.20.4 with ModDevGradle 2025-05-04 18:43:11 +00:00
EoD
bcb55c5a67 Revert "register JDBC driver to work around Forge bug"
This reverts commit ce0e173a9e.
2025-05-04 18:21:32 +00:00
20 changed files with 904 additions and 426 deletions

39
.github/workflows/backport-prs.yml vendored Normal file
View File

@ -0,0 +1,39 @@
name: Backport merged pull request
on:
pull_request_target:
types: [closed]
issue_comment:
types: [created]
permissions:
contents: write # so it can comment
pull-requests: write # so it can create pull requests
jobs:
backport:
name: Backport pull request
runs-on: ubuntu-latest
# Only run when pull request is merged
# or when a comment starting with `/backport` is created by someone other than the
# https://github.com/backport-action bot user (user id: 97796249). Note that if you use your
# own PAT as `github_token`, that you should replace this id with yours.
if: >
(
github.event_name == 'pull_request_target' &&
github.event.pull_request.merged
) || (
github.event_name == 'issue_comment' &&
github.event.issue.pull_request &&
github.event.comment.user.id != 97796249 &&
startsWith(github.event.comment.body, '/backport')
)
steps:
- uses: actions/checkout@v4
- name: Create backport pull requests
uses: korthout/backport-action@v3
with:
github_token: ${{ secrets.TOKEN }}
pull_description: |
Backport of #${pull_number} to `${target_branch}`.
### Description
${pull_description}

View File

@ -19,7 +19,7 @@ jobs:
uses: actions/checkout@v4
- name: Validate Gradle Wrapper
uses: gradle/actions/wrapper-validation@v4
uses: gradle/actions/wrapper-validation@v5
- name: Set up JDK 17
uses: actions/setup-java@v4
@ -28,7 +28,7 @@ jobs:
distribution: 'temurin'
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v4
uses: gradle/actions/setup-gradle@v5
- name: Build with Gradle
run: ./gradlew build

5
.gitignore vendored
View File

@ -31,6 +31,7 @@ build
# other
eclipse
run
runs
run-data
# Files from Forge MDK
forge*changelog.txt
repo

10
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,10 @@
{
"java.compile.nullAnalysis.mode": "automatic",
"java.completion.importOrder": [
"",
"javax",
"java",
"#"
],
"java.sources.organizeImports.starThreshold": 5
}

View File

@ -1,8 +1,8 @@
plugins {
id 'idea'
id 'java-library'
id 'maven-publish'
id 'net.neoforged.moddev.legacyforge' version '2.0.84'
id 'net.neoforged.moddev' version '2.0.86'
id 'idea'
}
tasks.named('wrapper', Wrapper).configure {
@ -19,6 +19,17 @@ group = mod_group_id
repositories {
mavenLocal()
exclusiveContent {
forRepository {
maven {
name = "Modrinth"
url = "https://api.modrinth.com/maven"
}
}
filter {
includeGroup "maven.modrinth"
}
}
exclusiveContent {
forRepository {
maven {
@ -35,12 +46,12 @@ base {
archivesName = mod_id
}
// Mojang ships Java 17 to end users in 1.20.1, so mods should target Java 17.
// Mojang ships Java 17 to end users from 1.18 to 1.20.4, so your mod should target Java 17.
java.toolchain.languageVersion = JavaLanguageVersion.of(17)
legacyForge {
// Specify the version of MinecraftForge to use.
version = project.minecraft_version + '-' + project.forge_version
neoForge {
// Specify the version of NeoForge to use.
version = project.neo_version
parchment {
mappingsVersion = project.parchment_mappings_version
@ -57,13 +68,13 @@ legacyForge {
client()
// Comma-separated list of namespaces to load gametests from. Empty = all namespaces.
systemProperty 'forge.enabledGameTestNamespaces', project.mod_id
systemProperty 'neoforge.enabledGameTestNamespaces', project.mod_id
}
server {
server()
programArgument '--nogui'
systemProperty 'forge.enabledGameTestNamespaces', project.mod_id
systemProperty 'neoforge.enabledGameTestNamespaces', project.mod_id
}
// This run config launches GameTestServer and runs all registered gametests, then exits.
@ -71,7 +82,7 @@ legacyForge {
// The gametest system is also enabled by default for other run configs under the /test command.
gameTestServer {
type = "gameTestServer"
systemProperty 'forge.enabledGameTestNamespaces', project.mod_id
systemProperty 'neoforge.enabledGameTestNamespaces', project.mod_id
}
data {
@ -103,8 +114,7 @@ legacyForge {
mods {
// define mod <-> source bindings
// these are used to tell the game which sources are for which mod
// mostly optional in a single mod project
// but multi mod projects should define one per mod
// multi mod projects should define one per mod
"${mod_id}" {
sourceSet(sourceSets.main)
}
@ -114,46 +124,38 @@ legacyForge {
// Include resources generated by data generators.
sourceSets.main.resources { srcDir 'src/generated/resources' }
// Sets up a dependency configuration called 'localRuntime' and a deobfuscating one called 'modLocalRuntime'
// These configurations should be used instead of 'runtimeOnly' to declare
// Sets up a dependency configuration called 'localRuntime'.
// This configuration should be used instead of 'runtimeOnly' to declare
// a dependency that will be present for runtime testing but that is
// "optional", meaning it will not be pulled by dependents of this mod.
configurations {
runtimeClasspath.extendsFrom localRuntime
}
obfuscation {
createRemappingConfiguration(configurations.localRuntime)
}
dependencies {
// If you wish to declare dependencies against mods, make sure to use the 'mod*' configurations so that they're remapped.
// See https://github.com/neoforged/ModDevGradle/blob/main/LEGACY.md#remapping-mod-dependencies for more information.
// Example optional mod dependency with JEI
// The JEI API is declared for compile time use, while the full JEI artifact is used at runtime
// modCompileOnly "mezz.jei:jei-${mc_version}-common-api:${jei_version}"
// modCompileOnly "mezz.jei:jei-${mc_version}-neoforge-api:${jei_version}"
// compileOnly "mezz.jei:jei-${mc_version}-common-api:${jei_version}"
// compileOnly "mezz.jei:jei-${mc_version}-neoforge-api:${jei_version}"
// We add the full version to localRuntime, not runtimeOnly, so that we do not publish a dependency on it
// modLocalRuntime "mezz.jei:jei-${mc_version}-neoforge:${jei_version}"
compileOnly "curse.maven:curios-309927:5266541"
compileOnly "curse.maven:sophisticated-backpacks-422301:6303388"
compileOnly "curse.maven:sophisticated-core-618298:6317048"
// localRuntime "mezz.jei:jei-${mc_version}-neoforge:${jei_version}"
compileOnly "maven.modrinth:curios:7.4.3+1.20.4"
compileOnly "curse.maven:sophisticated-backpacks-422301:5297718"
compileOnly "curse.maven:sophisticated-core-618298:5296142"
// Example mod dependency using a mod jar from ./libs with a flat dir repository
// This maps to ./libs/coolmod-${mc_version}-${coolmod_version}.jar
// The group id is ignored when searching -- in this case, it is "blank"
// modImplementation "blank:coolmod-${mc_version}:${coolmod_version}"
// implementation "blank:coolmod-${mc_version}:${coolmod_version}"
// Example mod dependency using a file as dependency
// modImplementation files("libs/coolmod-${mc_version}-${coolmod_version}.jar")
// implementation files("libs/coolmod-${mc_version}-${coolmod_version}.jar")
// Example project dependency using a sister or child project:
// modImplementation project(":myproject")
// implementation project(":myproject")
// embedd the JDBC driver in the mod using jarJar
// JDBC driver auto-detection is broken in Forge as of v47.4.0
// i.e. we need to need it both at compile and runtime
implementation "com.mysql:mysql-connector-j:${jdbc_version}"
runtimeOnly "com.mysql:mysql-connector-j:${jdbc_version}"
jarJar "com.mysql:mysql-connector-j:${jdbc_version}"
additionalRuntimeClasspath "com.mysql:mysql-connector-j:${jdbc_version}"
@ -162,39 +164,21 @@ dependencies {
// http://www.gradle.org/docs/current/userguide/dependency_management.html
}
// Uncomment the lines below if you wish to configure mixin. The mixin file should be named modid.mixins.json.
/*
mixin {
add sourceSets.main, "${mod_id}.refmap.json"
config "${mod_id}.mixins.json"
}
dependencies {
annotationProcessor 'org.spongepowered:mixin:0.8.5:processor'
}
jar {
manifest.attributes([
"MixinConfigs": "${mod_id}.mixins.json"
])
}
*/
// This block of code expands all declared replace properties in the specified resource targets.
// A missing property will result in an error. Properties are expanded using ${} Groovy notation.
var generateModMetadata = tasks.register("generateModMetadata", ProcessResources) {
var replaceProperties = [
minecraft_version : minecraft_version,
minecraft_version_range : minecraft_version_range,
forge_version : forge_version,
forge_version_range : forge_version_range,
loader_version_range : loader_version_range,
mod_id : mod_id,
mod_name : mod_name,
mod_license : mod_license,
mod_version : mod_version,
mod_authors : mod_authors,
mod_description : mod_description
minecraft_version : minecraft_version,
minecraft_version_range: minecraft_version_range,
neo_version : neo_version,
neo_version_range : neo_version_range,
loader_version_range : loader_version_range,
mod_id : mod_id,
mod_name : mod_name,
mod_license : mod_license,
mod_version : mod_version,
mod_authors : mod_authors,
mod_description : mod_description
]
inputs.properties replaceProperties
expand replaceProperties
@ -205,7 +189,7 @@ var generateModMetadata = tasks.register("generateModMetadata", ProcessResources
// this works with both building through Gradle and the IDE.
sourceSets.main.resources.srcDir generateModMetadata
// To avoid having to run "generateModMetadata" manually, make it run on every project reload
legacyForge.ideSyncTask generateModMetadata
neoForge.ideSyncTask generateModMetadata
// Example configuration to allow publishing using the maven-publish plugin
publishing {

View File

@ -7,22 +7,22 @@ org.gradle.configuration-cache=true
#read more on this at https://github.com/neoforged/ModDevGradle?tab=readme-ov-file#better-minecraft-parameter-names--javadoc-parchment
# you can also find the latest versions at: https://parchmentmc.org/docs/getting-started
parchment_minecraft_version=1.20.1
parchment_mappings_version=2023.09.03
parchment_minecraft_version=1.20.4
parchment_mappings_version=2024.04.14
# Environment Properties
# You can find the latest versions here: https://files.minecraftforge.net/net/minecraftforge/forge/index_1.20.1.html
# The Minecraft version must agree with the Forge version to get a valid artifact
minecraft_version=1.20.1
# You can find the latest versions here: https://projects.neoforged.net/neoforged/neoforge
# The Minecraft version must agree with the Neo version to get a valid artifact
minecraft_version=1.20.4
# The Minecraft version range can use any release version of Minecraft as bounds.
# Snapshots, pre-releases, and release candidates are not guaranteed to sort properly
# as they do not follow standard versioning conventions.
minecraft_version_range=[1.20.1, 1.21)
# The Forge version must agree with the Minecraft version to get a valid artifact
forge_version=47.4.0
# The Forge version range can use any version of Forge as bounds
forge_version_range=[47,)
minecraft_version_range=[1.20.4]
# The Neo version must agree with the Minecraft version to get a valid artifact
neo_version=20.4.248
# The Neo version range can use any version of Neo as bounds
neo_version_range=[20.4.248,)
# The loader version range can only use the major version of FML as bounds
loader_version_range=[47,)
loader_version_range=[1,)
## Mod Properties
@ -34,7 +34,7 @@ mod_name=PlayerSync
# The license of the mod. Review your options at https://choosealicense.com/. All Rights Reserved is the default.
mod_license=GPL-3.0 license
# The mod version. See https://semver.org/
mod_version=2.1.0
mod_version=2.1.4
# The group ID for the mod. It is only important when publishing as an artifact to a Maven repository.
# This should match the base package used for the mod sources.
# See https://maven.apache.org/guides/mini/guide-naming-conventions.html

View File

@ -1,3 +1,11 @@
pluginManagement {
repositories {
mavenLocal()
gradlePluginPortal()
maven { url = 'https://maven.neoforged.net/releases' }
}
}
plugins {
id 'org.gradle.toolchains.foojay-resolver-convention' version '0.9.0'
}

View File

@ -0,0 +1,26 @@
package vip.fubuki.playersync;
import com.mojang.brigadier.CommandDispatcher;
import net.minecraft.commands.CommandSourceStack;
import net.minecraft.commands.Commands;
import net.neoforged.bus.api.SubscribeEvent;
import net.neoforged.fml.common.Mod;
import net.neoforged.neoforge.event.RegisterCommandsEvent;
@Mod.EventBusSubscriber()
public class CommandInit {
@SubscribeEvent
public static void registerCommand(RegisterCommandsEvent event){
CommandDispatcher<CommandSourceStack> dispatcher=event.getDispatcher();
dispatcher.register(Commands.literal("playersync")
.requires(cs->cs.hasPermission(2))
.then(Commands.literal("reconnect")
.executes(context -> {
// context.getSource().sendSuccess(()->MutableComponent.create(new TranslatableContents("playersync.command.reconnect")),true);
return 0;
}
))
);
}
}

View File

@ -1,15 +1,17 @@
package vip.fubuki.playersync;
import com.mojang.logging.LogUtils;
import net.minecraftforge.common.MinecraftForge;
import net.minecraftforge.event.server.ServerStartingEvent;
import net.minecraftforge.eventbus.api.IEventBus;
import net.minecraftforge.eventbus.api.SubscribeEvent;
import net.minecraftforge.fml.ModList;
import net.minecraftforge.fml.common.Mod;
import net.minecraftforge.fml.config.ModConfig;
import net.minecraftforge.fml.event.lifecycle.FMLCommonSetupEvent;
import net.minecraftforge.fml.javafmlmod.FMLJavaModLoadingContext;
import net.minecraft.SharedConstants;
import net.neoforged.bus.api.IEventBus;
import net.neoforged.bus.api.SubscribeEvent;
import net.neoforged.fml.ModList;
import net.neoforged.fml.ModLoadingContext;
import net.neoforged.fml.common.Mod;
import net.neoforged.fml.config.ModConfig;
import net.neoforged.fml.event.lifecycle.FMLCommonSetupEvent;
import net.neoforged.neoforge.common.NeoForge;
import net.neoforged.neoforge.event.server.ServerStartingEvent;
import net.neoforged.neoforge.event.server.ServerStoppingEvent;
import org.slf4j.Logger;
import vip.fubuki.playersync.config.JdbcConfig;
import vip.fubuki.playersync.sync.ChatSync;
@ -17,33 +19,22 @@ import vip.fubuki.playersync.sync.VanillaSync;
import vip.fubuki.playersync.util.JDBCsetUp;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import com.mysql.cj.jdbc.Driver;
@Mod(PlayerSync.MODID)
public class PlayerSync {
public static final String MODID = "playersync";
public static final Logger LOGGER = LogUtils.getLogger();
public PlayerSync(FMLJavaModLoadingContext context) {
IEventBus modEventBus = context.getModEventBus();
context.registerConfig(ModConfig.Type.COMMON, JdbcConfig.COMMON_CONFIG);
public PlayerSync(IEventBus modEventBus) {
modEventBus.addListener(this::commonSetup);
MinecraftForge.EVENT_BUS.register(this);
NeoForge.EVENT_BUS.register(this);
ModLoadingContext.get().registerConfig(ModConfig.Type.COMMON, JdbcConfig.COMMON_CONFIG);
}
private void commonSetup(final FMLCommonSetupEvent event) {
// JDBC driver auto-detection is broken in Forge as of v47.4.0
// We need to register the driver manually
try {
DriverManager.registerDriver(new Driver());
} catch (SQLException e) {
LOGGER.error("Unable to register JDBC MySQL driver", e);
}
VanillaSync.register();
event.enqueueWork(() -> {
// read SYNC_CHAT only within the enqueueWork to reliably get the real
@ -60,7 +51,7 @@ public class PlayerSync {
String dbName = JdbcConfig.DATABASE_NAME.get();
// Step 1: Create the database using a connection that does not select a database.
JDBCsetUp.executeUpdate("CREATE DATABASE IF NOT EXISTS " + dbName, 1);
JDBCsetUp.executeUpdateWithoutDatabase("CREATE DATABASE IF NOT EXISTS " + dbName);
// Step 2: Explicitly select the database on a connection obtained without default database.
try (Connection conn = JDBCsetUp.getConnection(false);
@ -122,17 +113,38 @@ public class PlayerSync {
"PRIMARY KEY (`id`)" +
");"
);
// do not modify the create table statement to make sure this code is compatible with older database versions
addColumnIfNotExists("server_info", "data_version", "INT NOT NULL DEFAULT 0");
long current = System.currentTimeMillis();
JDBCsetUp.executeUpdate(
"INSERT INTO " + dbName + ".server_info(id,enable,last_update) " +
"VALUES(" + JdbcConfig.SERVER_ID.get() + ",true," + current + ") " +
"ON DUPLICATE KEY UPDATE id= " + JdbcConfig.SERVER_ID.get() + ",enable = 1," +
"last_update=" + current + ";"
);
JDBCsetUp.executeUpdate(
"UPDATE " + dbName + ".server_info SET last_update=" + System.currentTimeMillis() +
" WHERE id='" + JdbcConfig.SERVER_ID.get() + "'"
);
int data_version = SharedConstants.getCurrentVersion().getDataVersion().getVersion();
JDBCsetUp.executeUpdate("""
INSERT INTO %s.server_info
(
id,
enable,
data_version,
last_update
)
VALUES (
%d,
true,
%d,
%d
)
ON DUPLICATE KEY UPDATE
id = %d,
enable = true,
data_version = %d,
last_update = %d;
""",
dbName,
JdbcConfig.SERVER_ID.get(),
data_version,
current,
JdbcConfig.SERVER_ID.get(),
data_version,
current);
// Create curios table if the Curios mod is loaded
if (ModList.get().isLoaded("curios")) {
@ -145,28 +157,13 @@ public class PlayerSync {
// Create backpack_data table
if (ModList.get().isLoaded("sophisticatedbackpacks")) {
JDBCsetUp.executeUpdate(
JDBCsetUp.executeUpdateWithoutDatabase(
"CREATE TABLE IF NOT EXISTS " + dbName + ".backpack_data (" +
"uuid CHAR(36) NOT NULL, backpack_nbt MEDIUMBLOB, PRIMARY KEY (uuid)" +
");", 1
");"
);
// Check if backpack_data table has the 'uuid' column
JDBCsetUp.QueryResult backpackColCheck = JDBCsetUp.executeQuery(
"SELECT COUNT(*) AS colCount FROM INFORMATION_SCHEMA.COLUMNS " +
"WHERE TABLE_SCHEMA = '" + dbName + "' " +
"AND TABLE_NAME = 'backpack_data' " +
"AND COLUMN_NAME = 'uuid';"
);
ResultSet rsBackpackCol = backpackColCheck.resultSet();
if (rsBackpackCol.next() && rsBackpackCol.getInt("colCount") == 0) {
LOGGER.info("Altering backpack_data table to add missing 'uuid' column.");
// Add the missing column and set it as primary key.
JDBCsetUp.executeUpdate("ALTER TABLE " + dbName + ".backpack_data ADD COLUMN uuid CHAR(36) NOT NULL", 1);
JDBCsetUp.executeUpdate("ALTER TABLE " + dbName + ".backpack_data ADD PRIMARY KEY (uuid)", 1);
}
rsBackpackCol.close();
backpackColCheck.connection().close();
addColumnIfNotExists("backpack_data", "uuid", "CHAR(36) NOT NULL", true);
}
// Check and alter the 'advancements' column in player_data if necessary
@ -180,14 +177,64 @@ public class PlayerSync {
if (rsAdvCol.next()) {
String dataType = rsAdvCol.getString("DATA_TYPE");
if (!"mediumblob".equalsIgnoreCase(dataType)) {
LOGGER.info("Altering player_data table to modify 'advancements' column to MEDIUMBLOB.");
JDBCsetUp.executeUpdate("ALTER TABLE " + dbName + ".player_data MODIFY COLUMN advancements MEDIUMBLOB", 1);
LOGGER.info("Altering player_data table to modify 'advancements' column from {} to MEDIUMBLOB.", dataType);
JDBCsetUp.executeUpdateWithoutDatabase("ALTER TABLE " + dbName + ".player_data MODIFY COLUMN advancements MEDIUMBLOB");
}
}
rsAdvCol.close();
// ----- END NEW BLOCK -----
try {
JDBCsetUp.executeUpdate("UPDATE player_data SET online=0 WHERE last_server=" + JdbcConfig.SERVER_ID.get() +" AND online=1 LIMIT 1000");
} catch (Exception e) {
LOGGER.error("An exception occurred while trying change wrong player-status\n" + e.getMessage());
}
LOGGER.info("PlayerSync is ready!");
}
@SubscribeEvent
public void onServerStopping(ServerStoppingEvent event){
ChatSync.shutdown();
}
private static void addColumnIfNotExists(String tableName, String columnName, String dataTypeDefaultNullness,
boolean makePrimaryKey) throws SQLException {
// Making use of the AutoCloseable QueryResult here
try (JDBCsetUp.QueryResult backpackColCheck = JDBCsetUp.executeQuery(
"SELECT COUNT(*) AS colCount FROM INFORMATION_SCHEMA.COLUMNS " +
"WHERE TABLE_SCHEMA = DATABASE()" +
"AND TABLE_NAME = '" + tableName + "' " +
"AND COLUMN_NAME = '" + columnName + "';")) {
ResultSet rsBackpackCol = backpackColCheck.resultSet();
if (!rsBackpackCol.next()) {
LOGGER.warn("Warning: Unable to check existence of colum {} in table {}.", columnName, tableName);
return;
}
if (rsBackpackCol.getInt("colCount") > 0) {
LOGGER.debug("Column {} already exists. Skipping creation.", columnName);
return;
}
}
LOGGER.info("ALTER {} table to add missing {} column.", tableName, columnName);
// Add the missing column and set it as primary key.
JDBCsetUp.executeUpdate(
"ALTER TABLE %s ADD COLUMN %s %s",
tableName, columnName, dataTypeDefaultNullness);
if (makePrimaryKey) {
LOGGER.info("Altering {} table to add primary key on {}.", tableName, columnName);
JDBCsetUp.executeUpdate(
"ALTER TABLE %s ADD PRIMARY KEY (%s)",
tableName, columnName);
}
}
private static void addColumnIfNotExists(String tableName, String columnName,
String dataTypeDefaultNullness) throws SQLException {
addColumnIfNotExists(tableName, columnName, dataTypeDefaultNullness, false);
}
}

View File

@ -1,7 +1,7 @@
package vip.fubuki.playersync.config;
import net.minecraftforge.common.ForgeConfigSpec;
import net.neoforged.neoforge.common.ModConfigSpec;
import java.util.ArrayList;
import java.util.List;
@ -9,28 +9,28 @@ import java.util.Random;
public class JdbcConfig {
public static ForgeConfigSpec COMMON_CONFIG;
public static ForgeConfigSpec.ConfigValue<String> HOST;
public static ForgeConfigSpec.IntValue PORT;
public static ForgeConfigSpec.ConfigValue<String> USERNAME;
public static ForgeConfigSpec.ConfigValue<String> PASSWORD;
public static ForgeConfigSpec.ConfigValue<String> DATABASE_NAME;
public static ForgeConfigSpec.ConfigValue<List<String>> SYNC_WORLD;
public static ForgeConfigSpec.BooleanValue SYNC_ADVANCEMENTS;
public static ForgeConfigSpec.BooleanValue USE_SSL;
public static ForgeConfigSpec.BooleanValue SYNC_CHAT;
public static ForgeConfigSpec.BooleanValue IS_CHAT_SERVER;
public static final ForgeConfigSpec.ConfigValue<String> ITEM_PLACEHOLDER_TITLE_OVERRIDE;
public static final ForgeConfigSpec.ConfigValue<String> ITEM_PLACEHOLDER_DESCRIPTION_OVERRIDE;
public static ForgeConfigSpec.ConfigValue<String> CHAT_SERVER_IP;
public static ForgeConfigSpec.IntValue CHAT_SERVER_PORT;
public static ForgeConfigSpec.BooleanValue USE_LEGACY_SERIALIZATION;
public static final ModConfigSpec COMMON_CONFIG;
public static final ModConfigSpec.ConfigValue<String> HOST;
public static final ModConfigSpec.IntValue PORT;
public static final ModConfigSpec.ConfigValue<String> USERNAME;
public static final ModConfigSpec.ConfigValue<String> PASSWORD;
public static final ModConfigSpec.ConfigValue<String> DATABASE_NAME;
public static final ModConfigSpec.ConfigValue<List<String>> SYNC_WORLD;
public static final ModConfigSpec.BooleanValue SYNC_ADVANCEMENTS;
public static final ModConfigSpec.BooleanValue USE_SSL;
public static final ModConfigSpec.BooleanValue SYNC_CHAT;
public static final ModConfigSpec.BooleanValue IS_CHAT_SERVER;
public static final ModConfigSpec.ConfigValue<String> ITEM_PLACEHOLDER_TITLE_OVERRIDE;
public static final ModConfigSpec.ConfigValue<String> ITEM_PLACEHOLDER_DESCRIPTION_OVERRIDE;
public static final ModConfigSpec.ConfigValue<String> CHAT_SERVER_IP;
public static final ModConfigSpec.IntValue CHAT_SERVER_PORT;
public static final ModConfigSpec.BooleanValue USE_LEGACY_SERIALIZATION;
public static ForgeConfigSpec.ConfigValue<Integer> SERVER_ID;
public static final ModConfigSpec.ConfigValue<Integer> SERVER_ID;
static {
ForgeConfigSpec.Builder COMMON_BUILDER = new ForgeConfigSpec.Builder();
ModConfigSpec.Builder COMMON_BUILDER = new ModConfigSpec.Builder();
COMMON_BUILDER.comment("General settings").push("general");
HOST=COMMON_BUILDER.comment("The host of the database").define("host", "localhost");
PORT = COMMON_BUILDER.comment("database port").defineInRange("db_port", 3306, 0, 65535);

View File

@ -1,140 +1,55 @@
package vip.fubuki.playersync.sync;
import net.minecraft.network.chat.Component;
import net.minecraft.server.players.PlayerList;
import net.minecraftforge.common.MinecraftForge;
import net.minecraftforge.event.entity.player.PlayerEvent;
import net.minecraftforge.eventbus.api.SubscribeEvent;
import com.mojang.logging.LogUtils;
import net.neoforged.neoforge.common.NeoForge;
import org.slf4j.Logger;
import vip.fubuki.playersync.config.JdbcConfig;
import vip.fubuki.playersync.sync.chat.ChatSyncClient;
import vip.fubuki.playersync.sync.chat.ChatSyncServer;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Objects;
import java.util.Scanner;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import org.slf4j.Logger;
import com.mojang.logging.LogUtils;
public class ChatSync {
private static final Logger LOGGER = LogUtils.getLogger();
static PlayerList playerList;
static ServerSocket serverSocket;
static Socket clientSocket;
static Set<Socket> SocketList = ConcurrentHashMap.newKeySet();
static ExecutorService executorService = Executors.newCachedThreadPool();
public static final Logger LOGGER = LogUtils.getLogger();
private static ChatSyncServer chatSyncServer;
private static ChatSyncClient chatSyncClient;
public static void register(){
if(JdbcConfig.IS_CHAT_SERVER.get()) {
LOGGER.info("Launching chat server thread.");
new Thread(ChatSync::ServerSocket).start();
}
ClientSocket();
MinecraftForge.EVENT_BUS.register(ChatSync.class);
}
private static void ServerSocket() {
try {
LOGGER.info("Trying to setup chat server at port " + JdbcConfig.CHAT_SERVER_PORT.get());
serverSocket = new ServerSocket(JdbcConfig.CHAT_SERVER_PORT.get());
while (true) {
Socket newSocket = serverSocket.accept();
SocketList.add(newSocket);
executorService.submit(() -> handleClient(newSocket));
}
} catch (IOException e) {
LOGGER.error("Unable to start chat server");
e.printStackTrace();
} finally {
try {
serverSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
private static void handleClient(Socket socket) {
try (InputStream inputStream = socket.getInputStream()) {
byte[] buffer = new byte[1024];
int bytesRead;
while ((bytesRead = inputStream.read(buffer)) != -1) {
String message = new String(buffer, 0, bytesRead);
broadcastMessage(socket, message);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
SocketList.remove(socket);
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
private static void broadcastMessage(Socket sender, String message) {
for (Socket socket : SocketList) {
if (!socket.equals(sender)) {
new Thread(()->{
chatSyncServer = new ChatSyncServer();
try {
OutputStream outputStream = socket.getOutputStream();
outputStream.write(message.getBytes());
chatSyncServer.run();
} catch (IOException e) {
e.printStackTrace();
LOGGER.error("Unable to start chat server", e);
}
}
}, "ChatSync-Server").start();
}
}
private static void ClientSocket() {
try {
new Thread(()->{
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
LOGGER.info("Trying to connect to chat server "
+ JdbcConfig.CHAT_SERVER_IP.get()
+ ":"
+ JdbcConfig.CHAT_SERVER_PORT.get());
clientSocket = new Socket(JdbcConfig.CHAT_SERVER_IP.get(), JdbcConfig.CHAT_SERVER_PORT.get());
Scanner scanner = new Scanner(clientSocket.getInputStream());
while (scanner.hasNextLine()) {
String line = scanner.nextLine();
Component textComponents = Component.nullToEmpty(line);
playerList.broadcastSystemMessage(textComponents,true);
}
} catch (IOException e) {
e.printStackTrace();
reconnectClient();
chatSyncClient = new ChatSyncClient();
chatSyncClient.run();
}, "ChatSync-Client").start();
NeoForge.EVENT_BUS.register(ChatSyncClient.class);
}
public static void shutdown() {
if (chatSyncServer != null) {
chatSyncServer.shutdown();
}
if (chatSyncClient != null) {
chatSyncClient.shutdown();
}
}
private static void reconnectClient() {
LOGGER.warn("TODO: implement reconnectClient()");
//TODO
}
@SubscribeEvent
public static void onPlayerChat(net.minecraftforge.event.ServerChatEvent event) throws IOException {
String message= event.getUsername()+":"+event.getMessage();
OutputStream outputStream = clientSocket.getOutputStream();
outputStream.write(message.getBytes());
}
@SubscribeEvent
public static void onPlayerJoin(PlayerEvent.PlayerLoggedInEvent event){
playerList= Objects.requireNonNull(event.getEntity().getServer()).getPlayerList();
}
@SubscribeEvent
public static void onPlayerLeave(PlayerEvent.PlayerLoggedOutEvent event){
playerList= Objects.requireNonNull(event.getEntity().getServer()).getPlayerList();
}
}

View File

@ -5,8 +5,11 @@ import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.NbtUtils;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.ItemStack;
import net.minecraftforge.common.util.LazyOptional;
import net.minecraftforge.fml.ModList;
import net.neoforged.fml.ModList;
import top.theillusivec4.curios.api.CuriosApi;
import top.theillusivec4.curios.api.type.capability.ICuriosItemHandler;
import top.theillusivec4.curios.api.type.inventory.ICurioStacksHandler;
import top.theillusivec4.curios.api.type.inventory.IDynamicStackHandler;
import vip.fubuki.playersync.PlayerSync;
import vip.fubuki.playersync.util.JDBCsetUp;
import vip.fubuki.playersync.util.LocalJsonUtil;
@ -15,16 +18,9 @@ import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.Map;
import top.theillusivec4.curios.api.CuriosApi;
import top.theillusivec4.curios.api.type.capability.ICuriosItemHandler;
import top.theillusivec4.curios.api.type.inventory.ICurioStacksHandler;
import top.theillusivec4.curios.api.type.inventory.IDynamicStackHandler;
import java.util.Optional;
import java.util.UUID;
import static vip.fubuki.playersync.sync.VanillaSync.deserializeString;
public class ModsSupport {
@ -35,19 +31,13 @@ public class ModsSupport {
public void onPlayerJoin(net.minecraft.world.entity.player.Player player) throws SQLException {
if (ModList.get().isLoaded("curios")) {
// Obtain the handler from the API.
LazyOptional<ICuriosItemHandler> handlerOpt = CuriosApi.getCuriosInventory(player);
Optional<ICuriosItemHandler> handlerOpt = CuriosApi.getCuriosInventory(player);
JDBCsetUp.QueryResult qr = JDBCsetUp.executeQuery("SELECT curios_item FROM curios WHERE uuid = '" + player.getUUID() + "'");
ResultSet rs = qr.resultSet();
if (rs.next()) {
String curiosData = rs.getString("curios_item");
if (curiosData.length() <= 2) {
rs.close();
qr.connection().close();
return;
}
// Parse the stored data (assumes a simple Map.toString() format: "{key=value, key2=value2, ...}")
Map<String, String> storedMap = LocalJsonUtil.StringToMap(curiosData);
// Clear current Curios slots to avoid conflicts.
handlerOpt.ifPresent(handler -> handler.getCurios().forEach((slotType, stacksHandler) -> {
// Use the dynamic stack handler to clear slots.
@ -57,6 +47,12 @@ public class ModsSupport {
}
}));
if (curiosData.length() <= 2) {
rs.close();
qr.connection().close();
return;
}
// Restore each saved item.
handlerOpt.ifPresent(handler -> {
for (Map.Entry<String, String> entry : storedMap.entrySet()) {
@ -74,9 +70,7 @@ public class ModsSupport {
}
String serialized = entry.getValue();
try {
String nbtString = VanillaSync.deserializeString(serialized);
CompoundTag tag = NbtUtils.snbtToStructure(nbtString);
ItemStack stack = ItemStack.of(tag);
ItemStack stack = VanillaSync.deserializeAndCreatePlaceholderIfNeeded(serialized);
if (handler.getCurios().containsKey(slotType)) {
ICurioStacksHandler stacksHandler = handler.getCurios().get(slotType);
IDynamicStackHandler dynStacks = stacksHandler.getStacks();
@ -100,34 +94,35 @@ public class ModsSupport {
// --- Begin Backpack Data Restore ---
PlayerSync.LOGGER.info("Restoring backpack data for player " + player.getUUID());
net.p3pp3rf1y.sophisticatedbackpacks.util.PlayerInventoryProvider.get().runOnBackpacks(player, (ItemStack backpackItem, String handler, String identifier, int slot) -> {
backpackItem.getCapability(net.p3pp3rf1y.sophisticatedbackpacks.api.CapabilityBackpackWrapper.getCapabilityInstance())
.ifPresent(wrapper -> {
// Retrieve the contents UUID from the backpack's NBT using NBTHelper
Optional<UUID> uuidOpt = net.p3pp3rf1y.sophisticatedcore.util.NBTHelper.getUniqueId(wrapper.getBackpack(), "contentsUuid");
if (uuidOpt.isPresent()) {
UUID contentsUuid = uuidOpt.get();
try {
JDBCsetUp.QueryResult qrBackpack = JDBCsetUp.executeQuery("SELECT backpack_nbt FROM backpack_data WHERE uuid='" + contentsUuid + "'");
ResultSet rsBackpack = qrBackpack.resultSet();
if (rsBackpack.next()) {
String serialized = rsBackpack.getString("backpack_nbt");
String nbtString = deserializeString(serialized);
CompoundTag backpackNbt = NbtUtils.snbtToStructure(nbtString);
// Update BackpackStorage with the retrieved NBT
net.p3pp3rf1y.sophisticatedbackpacks.backpack.BackpackStorage.get().setBackpackContents(contentsUuid, backpackNbt);
PlayerSync.LOGGER.info("Restored backpack data for UUID " + contentsUuid);
}
rsBackpack.close();
qrBackpack.connection().close();
} catch (SQLException e) {
PlayerSync.LOGGER.error("Error restoring backpack data for UUID " + contentsUuid, e);
} catch (CommandSyntaxException e) {
throw new RuntimeException(e);
}
} else {
PlayerSync.LOGGER.warn("Backpack item in slot " + slot + " has no contentsUuid during restore");
}
});
net.p3pp3rf1y.sophisticatedbackpacks.backpack.wrapper.IBackpackWrapper backpackWrapper = net.p3pp3rf1y.sophisticatedbackpacks.backpack.wrapper.BackpackWrapper
.fromData(backpackItem);
// Retrieve the contents UUID from the backpack's NBT using NBTHelper
Optional<UUID> uuidOpt = net.p3pp3rf1y.sophisticatedcore.util.NBTHelper
.getUniqueId(backpackWrapper.getBackpack(), "contentsUuid");
if (uuidOpt.isPresent()) {
UUID contentsUuid = uuidOpt.get();
try {
JDBCsetUp.QueryResult qrBackpack = JDBCsetUp.executeQuery("SELECT backpack_nbt FROM backpack_data WHERE uuid='" + contentsUuid + "'");
ResultSet rsBackpack = qrBackpack.resultSet();
if (rsBackpack.next()) {
String serialized = rsBackpack.getString("backpack_nbt");
String nbtString = VanillaSync.deserializeString(serialized);
CompoundTag backpackNbt = NbtUtils.snbtToStructure(nbtString);
// Update BackpackStorage with the retrieved NBT
net.p3pp3rf1y.sophisticatedbackpacks.backpack.BackpackStorage.get().setBackpackContents(contentsUuid, backpackNbt);
PlayerSync.LOGGER.info("Restored backpack data for UUID " + contentsUuid);
}
rsBackpack.close();
qrBackpack.connection().close();
} catch (SQLException e) {
PlayerSync.LOGGER.error("Error restoring backpack data for UUID " + contentsUuid, e);
} catch (CommandSyntaxException e) {
throw new RuntimeException(e);
}
} else {
PlayerSync.LOGGER.warn("Backpack item in slot " + slot + " has no contentsUuid during restore");
}
return false;
});
// --- End Backpack Data Restore ---
@ -145,7 +140,7 @@ public class ModsSupport {
}
public void StoreCurios(net.minecraft.world.entity.player.Player player, boolean init) throws SQLException {
LazyOptional<ICuriosItemHandler> handlerOpt = CuriosApi.getCuriosInventory(player);
Optional<ICuriosItemHandler> handlerOpt = CuriosApi.getCuriosInventory(player);
Map<String, String> flatMap = new HashMap<>();
handlerOpt.ifPresent(handler -> {
@ -155,7 +150,7 @@ public class ModsSupport {
for (int i = 0; i < dynStacks.getSlots(); i++) {
ItemStack stack = dynStacks.getStackInSlot(i);
if (!stack.isEmpty()) {
String serialized = VanillaSync.serialize(stack.serializeNBT().toString());
String serialized = VanillaSync.serialize(VanillaSync.serializeNBT(stack).toString());
flatMap.put(slotType + ":" + i, serialized);
}
}
@ -173,26 +168,27 @@ public class ModsSupport {
public static void storeSophisticatedBackpacks(Player player) {
PlayerSync.LOGGER.info("Storing backpack data for player " + player.getUUID());
net.p3pp3rf1y.sophisticatedbackpacks.util.PlayerInventoryProvider.get().runOnBackpacks(player, (ItemStack backpackItem, String handler, String identifier, int slot) -> {
backpackItem.getCapability(net.p3pp3rf1y.sophisticatedbackpacks.api.CapabilityBackpackWrapper.getCapabilityInstance())
.ifPresent(wrapper -> {
// Retrieve the contents UUID from the backpack's NBT using NBTHelper
Optional<UUID> uuidOpt = net.p3pp3rf1y.sophisticatedcore.util.NBTHelper.getUniqueId(wrapper.getBackpack(), "contentsUuid");
if (uuidOpt.isPresent()) {
UUID contentsUuid = uuidOpt.get();
// Get internal backpack data from BackpackStorage (creates it if missing)
CompoundTag backpackNbt = net.p3pp3rf1y.sophisticatedbackpacks.backpack.BackpackStorage.get().getOrCreateBackpackContents(contentsUuid);
String serialized = VanillaSync.serialize(backpackNbt.toString());
try {
// Use REPLACE INTO so existing records are updated
JDBCsetUp.executeUpdate("REPLACE INTO backpack_data (uuid, backpack_nbt) VALUES ('" + contentsUuid + "', '" + serialized + "')");
PlayerSync.LOGGER.info("Saved backpack data for UUID " + contentsUuid);
} catch (SQLException e) {
PlayerSync.LOGGER.error("Error saving backpack data for UUID " + contentsUuid, e);
}
} else {
PlayerSync.LOGGER.warn("Backpack item in slot " + slot + " has no contentsUuid");
}
});
net.p3pp3rf1y.sophisticatedbackpacks.backpack.wrapper.IBackpackWrapper backpackWrapper = net.p3pp3rf1y.sophisticatedbackpacks.backpack.wrapper.BackpackWrapper
.fromData(backpackItem);
// Retrieve the contents UUID from the backpack's NBT using NBTHelper
Optional<UUID> uuidOpt = net.p3pp3rf1y.sophisticatedcore.util.NBTHelper
.getUniqueId(backpackWrapper.getBackpack(), "contentsUuid");
if (uuidOpt.isPresent()) {
UUID contentsUuid = uuidOpt.get();
// Get internal backpack data from BackpackStorage (creates it if missing)
CompoundTag backpackNbt = net.p3pp3rf1y.sophisticatedbackpacks.backpack.BackpackStorage.get().getOrCreateBackpackContents(contentsUuid);
String serialized = VanillaSync.serialize(backpackNbt.toString());
try {
// Use REPLACE INTO so existing records are updated
JDBCsetUp.executeUpdate("REPLACE INTO backpack_data (uuid, backpack_nbt) VALUES ('" + contentsUuid + "', '" + serialized + "')");
PlayerSync.LOGGER.info("Saved backpack data for UUID " + contentsUuid);
} catch (SQLException e) {
PlayerSync.LOGGER.error("Error saving backpack data for UUID " + contentsUuid, e);
}
} else {
PlayerSync.LOGGER.warn("Backpack item in slot " + slot + " has no contentsUuid");
}
return false; // Continue processing all backpack items.
});
}

View File

@ -1,19 +1,19 @@
package vip.fubuki.playersync.sync;
import com.mojang.brigadier.exceptions.CommandSyntaxException;
import com.mojang.serialization.Dynamic;
import net.minecraft.ChatFormatting;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.ListTag;
import net.minecraft.nbt.NbtUtils;
import net.minecraft.nbt.StringTag;
import net.minecraft.nbt.Tag;
import net.minecraft.SharedConstants;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.nbt.*;
import net.minecraft.network.chat.Component;
import net.minecraft.network.chat.Style;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.PlayerAdvancements;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.util.datafix.DataFixers;
import net.minecraft.util.datafix.fixes.References;
import net.minecraft.world.InteractionHand;
import net.minecraft.world.effect.MobEffect;
import net.minecraft.world.effect.MobEffectInstance;
@ -22,15 +22,14 @@ import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.Items;
import net.minecraft.world.level.storage.WorldData;
import net.minecraftforge.event.OnDatapackSyncEvent;
import net.minecraftforge.event.TickEvent;
import net.minecraftforge.event.entity.player.PlayerEvent;
import net.minecraftforge.event.server.ServerStoppedEvent;
import net.minecraftforge.eventbus.api.SubscribeEvent;
import net.minecraftforge.fml.ModList;
import net.minecraftforge.fml.common.Mod;
import net.minecraftforge.registries.ForgeRegistries;
import net.minecraftforge.server.ServerLifecycleHooks;
import net.neoforged.bus.api.SubscribeEvent;
import net.neoforged.fml.ModList;
import net.neoforged.fml.common.Mod;
import net.neoforged.neoforge.event.OnDatapackSyncEvent;
import net.neoforged.neoforge.event.TickEvent;
import net.neoforged.neoforge.event.entity.player.PlayerEvent;
import net.neoforged.neoforge.event.server.ServerStoppedEvent;
import net.neoforged.neoforge.server.ServerLifecycleHooks;
import vip.fubuki.playersync.PlayerSync;
import vip.fubuki.playersync.config.JdbcConfig;
import vip.fubuki.playersync.util.JDBCsetUp;
@ -43,11 +42,7 @@ import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.UUID;
import java.util.*;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
@ -150,6 +145,11 @@ public class VanillaSync {
JDBCsetUp.QueryResult qr1 = JDBCsetUp.executeQuery("SELECT online, last_server FROM player_data WHERE uuid='" + player_uuid + "'");
ResultSet rs1 = qr1.resultSet();
ServerPlayer serverPlayer = (ServerPlayer) event.getEntity();
// Mod support
ModsSupport modsSupport = new ModsSupport();
modsSupport.onPlayerJoin(serverPlayer);
if (!rs1.next()){
store(event.getEntity(), true);
return;
@ -184,10 +184,8 @@ public class VanillaSync {
// Restore basic attributes
serverPlayer.setHealth(rs2.getInt("health"));
serverPlayer.getFoodData().setFoodLevel(rs2.getInt("food_level"));
serverPlayer.totalExperience = 0;
serverPlayer.experienceLevel = 0;
serverPlayer.experienceProgress = 0;
serverPlayer.giveExperiencePoints(rs2.getInt("xp"));
setXpForPlayer(serverPlayer, rs2.getInt("xp"));
serverPlayer.setScore(rs2.getInt("score"));
// Restore left-hand item
@ -236,9 +234,6 @@ public class VanillaSync {
}
}
// Mod support
ModsSupport modsSupport = new ModsSupport();
modsSupport.onPlayerJoin(serverPlayer);
serverPlayer.addTag("player_synced");
rs2.close();
@ -256,7 +251,7 @@ public class VanillaSync {
}
// deserialize item and potentially create placeholders
private static ItemStack deserializeAndCreatePlaceholderIfNeeded(String serializedNbt)
public static ItemStack deserializeAndCreatePlaceholderIfNeeded(String serializedNbt)
throws CommandSyntaxException {
if (serializedNbt == null || serializedNbt.isEmpty() || serializedNbt.equals("B64:e30=")) {
// Check for empty NBT (Base64 encoded '{}')
@ -264,7 +259,7 @@ public class VanillaSync {
}
String nbtString = deserializeString(serializedNbt);
CompoundTag compoundTag = NbtUtils.snbtToStructure(nbtString);
CompoundTag compoundTag = snbtToFixedCompoundTag(nbtString);
if (compoundTag == null || compoundTag.isEmpty() || !compoundTag.contains("id", Tag.TAG_STRING)) {
return ItemStack.EMPTY; // Invalid or empty tag
@ -277,7 +272,7 @@ public class VanillaSync {
return ItemStack.EMPTY; // Cannot determine item type
}
if (ForgeRegistries.ITEMS.containsKey(registryName)) {
if (BuiltInRegistries.ITEM.containsKey(registryName)) {
// Item exists (could be vanilla or a loaded mod item), restore normally
try {
ItemStack restoredItem = ItemStack.of(compoundTag);
@ -286,7 +281,7 @@ public class VanillaSync {
// Either the item is not empty, or it is empty and the original tag was also
// empty or it was an empty inventory slot
if (!restoredItem.isEmpty() || compoundTag.isEmpty()
|| registryName.equals(ResourceLocation.parse("air"))) {
|| registryName.equals(ResourceLocation.tryParse("air"))) {
return restoredItem;
}
// ItemStack.of unexpectedly returned empty for a known, non-air item.
@ -357,6 +352,23 @@ public class VanillaSync {
return placeholder;
}
public static CompoundTag snbtToFixedCompoundTag(String nbtString) throws CommandSyntaxException {
CompoundTag parsedTag = TagParser.parseTag(nbtString);
int currentDataVersion = SharedConstants.getCurrentVersion().getDataVersion().getVersion();
int snbtDataVersion = NbtUtils.getDataVersion(parsedTag, 500);
Dynamic<Tag> dynamicTagInput = new Dynamic<>(NbtOps.INSTANCE, parsedTag);
Dynamic<Tag> updatedDynamicTag = DataFixers.getDataFixer().update(
References.ITEM_STACK,
dynamicTagInput,
snbtDataVersion,
currentDataVersion);
CompoundTag compoundTag = (CompoundTag) updatedDynamicTag.getValue();
return compoundTag;
}
/**
* Deserializes a string from the database back into an NBT string.
* Handles both the new Base64 format (prefixed with "B64:") and the old custom format.
@ -453,16 +465,28 @@ public class VanillaSync {
return itemStack.getTag().getString("playersync:original_item_nbt");
} else {
// It's a normal item or empty, serialize its current NBT
return serialize(itemStack.serializeNBT().toString());
return serialize(serializeNBT(itemStack).toString());
}
}
public static CompoundTag serializeNBT(ItemStack itemStack) {
if (itemStack == null || itemStack.isEmpty()) {
return new CompoundTag();
}
// Serialize the ItemStack to NBT
CompoundTag compoundTag = new CompoundTag();
itemStack.save(compoundTag);
// Adding data version to allow newer version of Minecraft to properly update the itemstack from the db
NbtUtils.addCurrentDataVersion(compoundTag);
return compoundTag;
}
public static void store(Player player, boolean init) throws SQLException, IOException {
String player_uuid = player.getUUID().toString();
PlayerSync.LOGGER.info("Storing data for player " + player_uuid + " (init=" + init + ")");
// Basic Attributes
int XP = player.totalExperience;
int XP = getTotalExperience(player);
int score = player.getScore();
int food_level = player.getFoodData().getFoodLevel();
int health = (int) player.getHealth();
@ -499,7 +523,7 @@ public class VanillaSync {
Map<Integer, String> effectMap = new HashMap<>();
for (Map.Entry<MobEffect, MobEffectInstance> entry : effects.entrySet()) {
CompoundTag effectTag = entry.getValue().save(new CompoundTag());
effectMap.put(MobEffect.getId(entry.getKey()), serialize(effectTag.toString()));
effectMap.put(BuiltInRegistries.MOB_EFFECT.getId(entry.getKey()), serialize(effectTag.toString()));
}
// Advancements
@ -625,4 +649,52 @@ public class VanillaSync {
}
}
}
private static void setXpForPlayer(ServerPlayer serverPlayer, int databaseXp) {
// Don't use giveExperience() as it has several side-effects:
// triggers an event, sends network packets, increases the score, ...
serverPlayer.totalExperience = databaseXp;
serverPlayer.experienceLevel = 0;
serverPlayer.experienceProgress = 0;
int xpForLevel;
while (databaseXp >= (xpForLevel = serverPlayer.getXpNeededForNextLevel())) {
databaseXp -= xpForLevel;
serverPlayer.experienceLevel++;
}
serverPlayer.experienceProgress = serverPlayer.experienceLevel > 0
? (float) databaseXp / serverPlayer.getXpNeededForNextLevel()
: 0f;
PlayerSync.LOGGER.debug("Giving player "
+ serverPlayer.experienceLevel + " levels and "
+ serverPlayer.experienceProgress * 100 + "% experience progress, calculated from "
+ serverPlayer.totalExperience + " XP.");
}
private static int getTotalExperience(final Player player) {
int level = player.experienceLevel;
int totalXp = 0;
// Calculate total XP for completed levels
if (level > 30) {
totalXp = (int) (4.5 * Math.pow(level, 2) - 162.5 * level + 2220);
} else if (level > 15) {
totalXp = (int) (2.5 * Math.pow(level, 2) - 40.5 * level + 360);
} else {
totalXp = level * level + 6 * level;
}
// Add partial level progress
totalXp += Math.round(player.getXpNeededForNextLevel() * player.experienceProgress);
PlayerSync.LOGGER.debug("Experience calcuation for "
+ player.experienceLevel + " levels and "
+ player.experienceProgress * 100 + "% experience progress yields "
+ totalXp + " XP.");
return totalXp;
}
}

View File

@ -0,0 +1,171 @@
package vip.fubuki.playersync.sync.chat;
import net.minecraft.network.chat.Component;
import net.minecraft.server.players.PlayerList;
import net.neoforged.bus.api.SubscribeEvent;
import net.neoforged.neoforge.event.ServerChatEvent;
import net.neoforged.neoforge.event.entity.player.PlayerEvent;
import vip.fubuki.playersync.PlayerSync;
import vip.fubuki.playersync.config.JdbcConfig;
import java.io.*;
import java.net.ConnectException;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.SocketTimeoutException;
import java.util.Objects;
public class ChatSyncClient {
static PlayerList playerList;
static Socket clientSocket;
static PrintWriter out;
private static volatile boolean running = true;
private static final int RECONNECT_DELAY = 5000;
private static final int MAX_RECONNECT_ATTEMPTS = 10;
private static volatile long lastHeartbeat = System.currentTimeMillis();
private static final long HEARTBEAT_INTERVAL = 15000;
public void run() {
int reconnectAttempts = 0;
while (running && reconnectAttempts < MAX_RECONNECT_ATTEMPTS) {
try {
PlayerSync.LOGGER.info("Connecting to chat server {}:{}",
JdbcConfig.CHAT_SERVER_IP.get(),
JdbcConfig.CHAT_SERVER_PORT.get());
clientSocket = new Socket();
clientSocket.setReuseAddress(true);
clientSocket.setKeepAlive(true);
clientSocket.setTcpNoDelay(true);
clientSocket.connect(
new InetSocketAddress(
JdbcConfig.CHAT_SERVER_IP.get(),
JdbcConfig.CHAT_SERVER_PORT.get()
),
15000
);
clientSocket.setSoTimeout(30000);
out = new PrintWriter(new BufferedWriter(
new OutputStreamWriter(clientSocket.getOutputStream())), true);
PlayerSync.LOGGER.info("Successfully connected to chat server");
reconnectAttempts = 0;
lastHeartbeat = System.currentTimeMillis();
startHeartbeatMonitor();
BufferedReader in = new BufferedReader(
new InputStreamReader(clientSocket.getInputStream()));
String serverMessage;
while (running && (serverMessage = in.readLine()) != null) {
lastHeartbeat = System.currentTimeMillis();
if ("<heartbeat>".equals(serverMessage)) {
continue;
}
PlayerSync.LOGGER.info("Received message from chat server: " + serverMessage);
Component textComponents = Component.nullToEmpty(serverMessage);
if(playerList != null){
playerList.getServer().execute(() ->
playerList.broadcastSystemMessage(textComponents, false));
}
}
} catch (SocketTimeoutException e) {
PlayerSync.LOGGER.warn("Chat server read timeout, reconnecting...");
} catch (ConnectException e) {
PlayerSync.LOGGER.warn("Cannot connect to chat server: {}", e.getMessage());
} catch (IOException e) {
PlayerSync.LOGGER.error("Chat client connection error: {}", e.getMessage());
} finally {
closeConnection();
}
if (running && reconnectAttempts < MAX_RECONNECT_ATTEMPTS) {
reconnectAttempts++;
PlayerSync.LOGGER.warn("Attempting to reconnect to chat server ({}/{})",
reconnectAttempts, MAX_RECONNECT_ATTEMPTS);
try {
long delay = Math.min(RECONNECT_DELAY * (long)Math.pow(2, reconnectAttempts-1), 60000);
Thread.sleep(delay);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
}
}
}
private void startHeartbeatMonitor() {
Thread heartbeatThread = new Thread(() -> {
while (running && clientSocket != null && !clientSocket.isClosed()) {
try {
Thread.sleep(10000); // 每10秒检查一次
long now = System.currentTimeMillis();
if (now - lastHeartbeat > HEARTBEAT_INTERVAL) {
PlayerSync.LOGGER.warn("No heartbeat for {}ms, sending test message",
now - lastHeartbeat);
// 发送测试消息检查连接
if (out != null) {
out.println("<heartbeat>");
}
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
}
}, "ChatSync-Heartbeat");
heartbeatThread.setDaemon(true);
heartbeatThread.start();
}
private void closeConnection() {
try {
if (out != null) {
out.close();
out = null;
}
if (clientSocket != null && !clientSocket.isClosed()) {
clientSocket.close();
clientSocket = null;
}
} catch (IOException e) {
PlayerSync.LOGGER.error("Error closing connection: {}", e.getMessage());
}
}
public void shutdown() {
running = false;
closeConnection();
}
@SubscribeEvent
public static void onPlayerChat(ServerChatEvent event) {
String message= "<"+event.getUsername()+"> "+event.getMessage().getString();
if (out != null) {
out.println(message);
}
}
@SubscribeEvent
public static void onPlayerJoin(PlayerEvent.PlayerLoggedInEvent event){
playerList = Objects.requireNonNull(event.getEntity().getServer()).getPlayerList();
}
@SubscribeEvent
public static void onPlayerLeave(PlayerEvent.PlayerLoggedOutEvent event){
playerList = Objects.requireNonNull(event.getEntity().getServer()).getPlayerList();
}
}

View File

@ -0,0 +1,171 @@
package vip.fubuki.playersync.sync.chat;
import vip.fubuki.playersync.PlayerSync;
import vip.fubuki.playersync.config.JdbcConfig;
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketTimeoutException;
import java.util.Iterator;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
public class ChatSyncServer {
static ServerSocket serverSocket;
static final Set<Socket> SocketList = ConcurrentHashMap.newKeySet();
static final ExecutorService executorService = Executors.newCachedThreadPool();
private volatile boolean running = true;
public void run() throws IOException {
try {
serverSocket = new ServerSocket(JdbcConfig.CHAT_SERVER_PORT.get());
serverSocket.setReuseAddress(true);
PlayerSync.LOGGER.info("Chat server started successfully on port {}", JdbcConfig.CHAT_SERVER_PORT.get());
startHeartbeatBroadcast();
while (running && !Thread.currentThread().isInterrupted()) {
try {
Socket newSocket = serverSocket.accept();
newSocket.setSoTimeout(30000);
SocketList.add(newSocket);
executorService.submit(() -> handleClient(newSocket));
PlayerSync.LOGGER.info("New client connected, total clients: {}", SocketList.size());
} catch (IOException e) {
if (running) {
PlayerSync.LOGGER.error("Error accepting client connection: {}", e.getMessage());
}
}
}
} finally {
shutdown();
}
}
private void handleClient(Socket socket) {
String clientInfo = socket.getInetAddress() + ":" + socket.getPort();
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(socket.getInputStream()))) {
String message;
while (running && (message = reader.readLine()) != null) {
PlayerSync.LOGGER.info("Received message from {}: {}", clientInfo, message);
broadcastMessage(socket, message);
}
} catch (SocketTimeoutException e) {
PlayerSync.LOGGER.warn("Client {} timeout", clientInfo);
} catch (IOException e) {
PlayerSync.LOGGER.error("Error handling client {}: {}", clientInfo, e.getMessage());
} finally {
SocketList.remove(socket);
try {
if (!socket.isClosed()) {
socket.close();
}
} catch (IOException e) {
PlayerSync.LOGGER.error("Error closing client socket: {}", e.getMessage());
}
PlayerSync.LOGGER.info("Client disconnected, remaining clients: {}", SocketList.size());
}
}
private void broadcastMessage(Socket sender, String message) {
Iterator<Socket> iterator = SocketList.iterator();
while (iterator.hasNext()) {
Socket socket = iterator.next();
if (!socket.equals(sender) && !socket.isClosed()) {
try {
PrintWriter writer = new PrintWriter(socket.getOutputStream(), true);
writer.println(message);
} catch (IOException e) {
PlayerSync.LOGGER.error("Error broadcasting to client, removing: {}", e.getMessage());
iterator.remove();
try {
socket.close();
} catch (IOException ex) {
// Ignore
}
}
}
}
}
private void startHeartbeatBroadcast() {
Thread heartbeatThread = new Thread(() -> {
while (running) {
try {
Thread.sleep(20000);
broadcastHeartbeat();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
}
}, "ChatSync-Server-Heartbeat");
heartbeatThread.setDaemon(true);
heartbeatThread.start();
}
private void broadcastHeartbeat() {
Iterator<Socket> iterator = SocketList.iterator();
while (iterator.hasNext()) {
Socket socket = iterator.next();
if (!socket.isClosed()) {
try {
PrintWriter writer = new PrintWriter(
new BufferedWriter(
new OutputStreamWriter(socket.getOutputStream())), true);
writer.println("<heartbeat>");
} catch (IOException e) {
PlayerSync.LOGGER.warn("Failed to send heartbeat to client, removing: {}", e.getMessage());
iterator.remove();
try {
socket.close();
} catch (IOException ex) {
// Ignore
}
}
} else {
iterator.remove();
}
}
}
public void shutdown() {
running = false;
try {
if (serverSocket != null && !serverSocket.isClosed()) {
serverSocket.close();
}
} catch (IOException e) {
PlayerSync.LOGGER.error("Error closing server socket: {}", e.getMessage());
}
for (Socket socket : SocketList) {
try {
if (!socket.isClosed()) {
socket.close();
}
} catch (IOException e) {
// Ignore
}
}
SocketList.clear();
executorService.shutdown();
try {
if (!executorService.awaitTermination(5, TimeUnit.SECONDS)) {
executorService.shutdownNow();
}
} catch (InterruptedException e) {
executorService.shutdownNow();
Thread.currentThread().interrupt();
}
}
}

View File

@ -1,13 +1,11 @@
package vip.fubuki.playersync.util;
import com.mojang.logging.LogUtils;
import org.slf4j.Logger;
import vip.fubuki.playersync.config.JdbcConfig;
import java.sql.*;
import org.slf4j.Logger;
import com.mojang.logging.LogUtils;
public class JDBCsetUp {
private static final Logger LOGGER = LogUtils.getLogger();
@ -45,7 +43,8 @@ public class JDBCsetUp {
/**
* Executes a query using a connection that includes the database.
*/
public static QueryResult executeQuery(String sql) throws SQLException {
public static QueryResult executeQuery(String sqlFormatString, Object... args) throws SQLException {
String sql = String.format(sqlFormatString, args);
LOGGER.trace(sql);
Connection connection = getConnection(); // With database selected (and "USE" already run)
PreparedStatement queryStatement = connection.prepareStatement(sql);
@ -54,28 +53,31 @@ public class JDBCsetUp {
}
/**
* Executes an update using a connection that includes the database.
* Executes an update using a connection with or without the database within the JDBC URL
*/
public static void executeUpdate(String sql) throws SQLException {
private static void executeUpdate(boolean selectDatabase, String sqlFormatString, Object... args) throws SQLException {
String sql = String.format(sqlFormatString, args);
LOGGER.trace(sql);
try (Connection connection = getConnection()) { // With database selected
try (Connection connection = getConnection(selectDatabase)) {
try (PreparedStatement updateStatement = connection.prepareStatement(sql)) {
updateStatement.executeUpdate();
}
}
}
/**
* Executes an update using a connection that includes the database in the JDBC URL
*/
public static void executeUpdate(String sqlFormatString, Object... args) throws SQLException {
executeUpdate(true, sqlFormatString, args);
}
/**
* Executes an update using a connection that does NOT include a default database.
* This method is used for commands like "CREATE DATABASE IF NOT EXISTS ..."
*/
public static void executeUpdate(String sql, int dummy) throws SQLException {
LOGGER.trace(sql);
try (Connection connection = getConnection(false)) { // Without default database
try (PreparedStatement updateStatement = connection.prepareStatement(sql)) {
updateStatement.executeUpdate();
}
}
public static void executeUpdateWithoutDatabase(String sqlFormatString, Object... args) throws SQLException {
executeUpdate(false, sqlFormatString, args);
}
/**
@ -92,6 +94,24 @@ public class JDBCsetUp {
}
}
public record QueryResult(Connection connection, ResultSet resultSet) {
public record QueryResult(Connection connection, ResultSet resultSet) implements AutoCloseable {
@Override
public void close() {
if (resultSet != null) {
try {
resultSet.close();
} catch (SQLException e) {
LOGGER.error("Error closing ResultSet", e);
}
}
if (connection != null) {
try {
connection.close();
} catch (SQLException e) {
LOGGER.error("Error closing Connection", e);
}
}
}
}
}

View File

@ -2,31 +2,44 @@ package vip.fubuki.playersync.util;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;
public class LocalJsonUtil {
public static Map<String,String> StringToMap(String param) {
Map<String,String> map = new HashMap<>();
String s1 = param.substring(1,param.length()-1);
String s2 = s1.trim();
String[] split = s2.split(",");
for (int i = split.length - 1; i >= 0; i--) {
String trim = split[i].trim();
String[] split1 = trim.split("=");
map.put(split1[0],split1[1]);
private static <K> Map<K, String> stringToGenericMap(String param, Function<String, K> keyParser) {
Map<K, String> map = new HashMap<>();
// check if string is at least minimal json
if (param == null || param.length() < 2 || param.equals("{}")) {
return map;
}
// extract string within outermost json brackets {}
String s1 = param.substring(param.indexOf('{')+1, param.lastIndexOf('}')).trim();
if (s1.isEmpty()) {
return map;
}
// split all json elements
for (String split : s1.split(",")) {
String trim = split.trim();
// only check for the first "=" as the values also contain additional "="
int equalIndex = trim.indexOf('=');
if (equalIndex < 0)
continue;
String key = trim.substring(0, equalIndex);
String value = trim.substring(equalIndex + 1);
map.put(keyParser.apply(key), value);
}
return map;
}
public static Map<Integer,String> StringToEntryMap(String param) {
Map<Integer,String> map = new HashMap<>();
String s1 = param.substring(1,param.length()-1);
String s2 = s1.trim();
String[] split = s2.split(",");
for (int i = split.length - 1; i >= 0; i--) {
String trim = split[i].trim();
String[] split1 = trim.split("=");
map.put(Integer.parseInt(split1[0]),split1[1]);
}
return map;
public static Map<String, String> StringToMap(String param) {
return stringToGenericMap(param, Function.identity());
}
public static Map<Integer, String> StringToEntryMap(String param) {
return stringToGenericMap(param, Integer::parseInt);
}
}

View File

@ -1,5 +1,5 @@
{
"playersync.item_placeholder_description": "Item is unknown on this server. This can either\nbe a modded item, an added, or a removed vanilla\nitem.\nThis voucher will automatically be replaced with\nthe corresponding item when joining a server\nwhere the item is known.","playersync.placeholder_titel_override": "Item Voucher",
"playersync.item_placeholder_description": "Item is unknown on this server. This can either\nbe a modded item, an added, or a removed vanilla\nitem.\nThis voucher will automatically be replaced with\nthe corresponding item when joining a server\nwhere the item is known.",
"playersync.item_placeholder_title": "Item Voucher",
"playersync.already_online": "You can't join more than one synchronization server at the same time."
}

View File

@ -1,3 +1,5 @@
{
"playersync.item_placeholder_description": "物品在此服务器未知。这可能是一个模组物品,或是不同版本的原版物品。\n这张券将会在加入可识别此物品的服务器后自动替换为对应物品。",
"playersync.item_placeholder_title": "物品券",
"playersync.already_online": "你不能同时加入多个同步的服务器。"
}
}

View File

@ -6,8 +6,8 @@
# The name of the mod loader type to load - for regular FML @Mod mods it should be javafml
modLoader="javafml" #mandatory
# A version range to match for said mod loader - for regular FML @Mod it will be the forge version
loaderVersion="${loader_version_range}" #mandatory This is typically bumped every Minecraft version by Forge. See our download page for lists of versions.
# A version range to match for said mod loader - for regular FML @Mod it will be the FML version. This is currently 2.
loaderVersion="${loader_version_range}" #mandatory
# The license for you mod. This is mandatory metadata and allows for easier comprehension of your redistributive properties.
# Review your options at https://choosealicense.com/. All rights reserved is the default copyright stance, and is thus the default here.
@ -28,7 +28,7 @@ version="${mod_version}" #mandatory
# A display name for the mod
displayName="${mod_name}" #mandatory
# A URL to query for updates for this mod. See the JSON update specification https://docs.minecraftforge.net/en/latest/misc/updatechecker/
# A URL to query for updates for this mod. See the JSON update specification https://docs.neoforged.net/docs/misc/updatechecker/
#updateJSONURL="https://change.me.example.invalid/updates.json" #optional
# A URL for the "homepage" for this mod, displayed in the mod UI
@ -36,34 +36,37 @@ displayURL="https://github.com/mlus-asuka/PlayerSync"
# A file name (in the root of the mod JAR) containing a logo for display
logoFile="logo.png" #optional
# A text field displayed in the mod UI
credits="Based on Mysql" #optional
# A text field displayed in the mod UI
authors="${mod_authors}" #optional
# Display Test controls the display for your mod in the server connection screen
# MATCH_VERSION means that your mod will cause a red X if the versions on client and server differ. This is the default behaviour and should be what you choose if you have server and client elements to your mod.
# IGNORE_SERVER_VERSION means that your mod will not cause a red X if it's present on the server but not on the client. This is what you should use if you're a server only mod.
# IGNORE_ALL_VERSION means that your mod will not cause a red X if it's present on the client or the server. This is a special case and should only be used if your mod has no server component.
# NONE means that no display test is set on your mod. You need to do this yourself, see IExtensionPoint.DisplayTest for more information. You can define any scheme you wish with this value.
# IMPORTANT NOTE: this is NOT an instruction as to which environments (CLIENT or DEDICATED SERVER) your mod loads on. Your mod should load (and maybe do nothing!) whereever it finds itself.
displayTest="IGNORE_SERVER_VERSION"
# The description text for the mod (multi line!) (#mandatory)
description='''${mod_description}'''
# The [[mixins]] block allows you to declare your mixin config to FML so that it gets loaded.
#[[mixins]]
#config="${mod_id}.mixins.json"
# The [[accessTransformers]] block allows you to declare where your AT file is.
# If this block is omitted, a fallback attempt will be made to load an AT from META-INF/accesstransformer.cfg
#[[accessTransformers]]
#file="META-INF/accesstransformer.cfg"
# The coremods config file path is not configurable and is always loaded from META-INF/coremods.json
# A dependency - use the . to indicate dependency for a specific modid. Dependencies are optional.
[[dependencies.${mod_id}]] #optional
# the modid of the dependency
modId="forge" #mandatory
# Does this dependency have to exist - if not, ordering below must be specified
mandatory=true #mandatory
modId="neoforge" #mandatory
# The type of the dependency. Can be one of "required", "optional", "incompatible" or "discouraged" (case insensitive).
# 'required' requires the mod to exist, 'optional' does not
# 'incompatible' will prevent the game from loading when the mod exists, and 'discouraged' will show a warning
type="required" #mandatory
# Optional field describing why the dependency is required or why it is incompatible
# reason="..."
# The version range of the dependency
versionRange="${forge_version_range}" #mandatory
versionRange="${neo_version_range}" #mandatory
# An ordering relationship for the dependency.
# BEFORE - This mod is loaded BEFORE the dependency
# AFTER - This mod is loaded AFTER the dependency
@ -74,7 +77,7 @@ description='''${mod_description}'''
# Here's another dependency
[[dependencies.${mod_id}]]
modId="minecraft"
mandatory=true
type="required"
# This version range declares a minimum of the current minecraft version up to but not including the next major version
versionRange="${minecraft_version_range}"
ordering="NONE"