Merge branch '1.20.4' into 1.20.4-dev

This commit is contained in:
mlus 2025-10-14 16:51:05 +08:00 committed by GitHub
commit 89b48fc3ff
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 251 additions and 103 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}

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,6 +1,7 @@
package vip.fubuki.playersync;
import com.mojang.logging.LogUtils;
import net.minecraft.SharedConstants;
import net.neoforged.bus.api.IEventBus;
import net.neoforged.bus.api.SubscribeEvent;
import net.neoforged.fml.ModList;
@ -50,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);
@ -112,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")) {
@ -135,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
@ -170,8 +177,8 @@ 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();
@ -185,4 +192,44 @@ public class PlayerSync {
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,31 +1,32 @@
package vip.fubuki.playersync.config;
import net.neoforged.neoforge.common.ModConfigSpec;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import net.neoforged.neoforge.common.ModConfigSpec;
public class JdbcConfig {
public static ModConfigSpec COMMON_CONFIG;
public static ModConfigSpec.ConfigValue<String> HOST;
public static ModConfigSpec.IntValue PORT;
public static ModConfigSpec.ConfigValue<String> USERNAME;
public static ModConfigSpec.ConfigValue<String> PASSWORD;
public static ModConfigSpec.ConfigValue<String> DATABASE_NAME;
public static ModConfigSpec.ConfigValue<List<String>> SYNC_WORLD;
public static ModConfigSpec.BooleanValue SYNC_ADVANCEMENTS;
public static ModConfigSpec.BooleanValue USE_SSL;
public static ModConfigSpec.BooleanValue SYNC_CHAT;
public static ModConfigSpec.BooleanValue IS_CHAT_SERVER;
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 ModConfigSpec.ConfigValue<String> CHAT_SERVER_IP;
public static ModConfigSpec.IntValue CHAT_SERVER_PORT;
public static ModConfigSpec.BooleanValue USE_LEGACY_SERIALIZATION;
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 ModConfigSpec.ConfigValue<Integer> SERVER_ID;
public static final ModConfigSpec.ConfigValue<Integer> SERVER_ID;
static {

View File

@ -6,6 +6,10 @@ import net.minecraft.nbt.NbtUtils;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.ItemStack;
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;
@ -14,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 {
@ -39,14 +36,8 @@ public class ModsSupport {
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.
@ -56,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()) {
@ -73,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();
@ -112,7 +107,7 @@ public class ModsSupport {
ResultSet rsBackpack = qrBackpack.resultSet();
if (rsBackpack.next()) {
String serialized = rsBackpack.getString("backpack_nbt");
String nbtString = deserializeString(serialized);
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);

View File

@ -1,7 +1,9 @@
package vip.fubuki.playersync.sync;
import com.mojang.brigadier.exceptions.CommandSyntaxException;
import com.mojang.serialization.Dynamic;
import net.minecraft.ChatFormatting;
import net.minecraft.SharedConstants;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.nbt.*;
import net.minecraft.network.chat.Component;
@ -10,6 +12,8 @@ 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;
@ -247,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 '{}')
@ -255,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
@ -348,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.
@ -455,6 +476,8 @@ public class VanillaSync {
// 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;
}
@ -463,7 +486,7 @@ public class VanillaSync {
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();

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);
}
}