Add mod compatibility: Accessories (Aether), Cosmetic Armor, Apotheosis
- Add Accessories API sync for Aether mod accessory slots (pendant, cape, gloves, rings, shield, misc). Uses same pattern as Curios: validate data before clearing slots, PreparedStatements for DB operations - Add Cosmetic Armor Reworked sync for 4 cosmetic armor slots via InventoryManager/CosArmorAPI - Add Apotheosis + Placebo as compileOnly deps. Apotheosis item data (affixes, gems, sockets, rarity) travels with items via DataComponents and is already synced by the inventory sync - New generic mod_player_data DB table with composite key (uuid, mod_id) for extensible mod-specific data storage - Integrated save/restore in join, logout, and auto-save pipelines Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
03b57c3e6b
commit
c63d5849a3
|
|
@ -121,6 +121,15 @@ dependencies {
|
|||
compileOnly "thedarkcolour:kotlinforforge:5.10.0"
|
||||
compileOnly "curse.maven:cobblemon-687131:7273151"
|
||||
|
||||
// Mod compatibility - Cosmetic Armor Reworked
|
||||
compileOnly "curse.maven:cosmetic-armor-reworked-237307:5610814"
|
||||
// Mod compatibility - Apotheosis + Placebo
|
||||
compileOnly "curse.maven:apotheosis-313970:7444906"
|
||||
compileOnly "curse.maven:placebo-283644:6926281"
|
||||
// Mod compatibility - The Aether + Accessories API
|
||||
compileOnly "curse.maven:aether-255308:7043502"
|
||||
compileOnly "curse.maven:accessories-938917:7046407"
|
||||
|
||||
runtimeOnly "curse.maven:curios-309927:6529130"
|
||||
runtimeOnly "curse.maven:sophisticated-backpacks-422301:7169832"
|
||||
runtimeOnly "curse.maven:sophisticated-core-618298:7168230"
|
||||
|
|
|
|||
|
|
@ -198,6 +198,16 @@ public class PlayerSync {
|
|||
rsAdvCol.close();
|
||||
// ----- END NEW BLOCK -----
|
||||
|
||||
// Create generic mod_player_data table for mod compatibility (Accessories, CosmeticArmor, Aether, etc.)
|
||||
JDBCsetUp.executeUpdate(
|
||||
"CREATE TABLE IF NOT EXISTS `" + dbName + "`.`mod_player_data` (" +
|
||||
"`uuid` CHAR(36) NOT NULL," +
|
||||
"`mod_id` VARCHAR(64) NOT NULL," +
|
||||
"`data_value` MEDIUMBLOB," +
|
||||
"PRIMARY KEY (`uuid`, `mod_id`)" +
|
||||
");"
|
||||
);
|
||||
|
||||
try {
|
||||
JDBCsetUp.executeUpdate("UPDATE player_data SET online=0 WHERE last_server=" + JdbcConfig.SERVER_ID.get() +" AND online=1 LIMIT 1000");
|
||||
} catch (Exception e) {
|
||||
|
|
|
|||
|
|
@ -43,6 +43,7 @@ import net.neoforged.neoforge.server.ServerLifecycleHooks;
|
|||
import vip.fubuki.playersync.PlayerSync;
|
||||
import vip.fubuki.playersync.config.JdbcConfig;
|
||||
import vip.fubuki.playersync.sync.addons.CuriosCache;
|
||||
import vip.fubuki.playersync.sync.addons.ModCompatSync;
|
||||
import vip.fubuki.playersync.sync.addons.ModsSupport;
|
||||
import vip.fubuki.playersync.util.JDBCsetUp;
|
||||
import vip.fubuki.playersync.util.LocalJsonUtil;
|
||||
|
|
@ -351,6 +352,8 @@ public class VanillaSync {
|
|||
if (ModList.get().isLoaded("sophisticatedstorage")) {
|
||||
ModsSupport.restoreSophisticatedStorageItems(serverPlayer);
|
||||
}
|
||||
// Restore mod compatibility data (Accessories/Aether, CosmeticArmor)
|
||||
ModCompatSync.restoreAll(serverPlayer);
|
||||
|
||||
serverPlayer.addTag("player_synced");
|
||||
|
||||
|
|
@ -613,6 +616,9 @@ public class VanillaSync {
|
|||
modsSupport.onPlayerLeave(player);
|
||||
}
|
||||
|
||||
// Save mod compatibility data (Accessories/Aether, CosmeticArmor)
|
||||
ModCompatSync.storeAll(player);
|
||||
|
||||
executorService.submit(() -> {
|
||||
try {
|
||||
doPlayerLogout(event);
|
||||
|
|
@ -859,6 +865,16 @@ public class VanillaSync {
|
|||
PlayerSync.LOGGER.error("Error auto-saving Curios data for player {}", player.getUUID(), e);
|
||||
}
|
||||
});
|
||||
// Auto-save mod compatibility data (Accessories, CosmeticArmor)
|
||||
executorService.submit(() -> {
|
||||
try {
|
||||
if (!player.isDeadOrDying()) {
|
||||
ModCompatSync.storeAll(player);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
PlayerSync.LOGGER.error("Error auto-saving mod compat data for player {}", player.getUUID(), e);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,276 @@
|
|||
package vip.fubuki.playersync.sync.addons;
|
||||
|
||||
import com.mojang.brigadier.exceptions.CommandSyntaxException;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.neoforged.fml.ModList;
|
||||
import vip.fubuki.playersync.PlayerSync;
|
||||
import vip.fubuki.playersync.sync.VanillaSync;
|
||||
import vip.fubuki.playersync.util.JDBCsetUp;
|
||||
import vip.fubuki.playersync.util.LocalJsonUtil;
|
||||
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Mod compatibility handlers for syncing player data from:
|
||||
* - Accessories API (used by The Aether for pendant, cape, gloves, rings, etc.)
|
||||
* - Cosmetic Armor Reworked (4 cosmetic armor slots)
|
||||
* - Apotheosis (item DataComponents travel with inventory - automatic)
|
||||
*/
|
||||
public class ModCompatSync {
|
||||
|
||||
// ============================
|
||||
// Accessories API (Aether slots)
|
||||
// ============================
|
||||
|
||||
/**
|
||||
* Saves Accessories inventory (used by The Aether and other mods).
|
||||
* Works identically to Curios sync but uses the Accessories API.
|
||||
*/
|
||||
public static void storeAccessories(Player player) {
|
||||
if (!ModList.get().isLoaded("accessories")) return;
|
||||
|
||||
try {
|
||||
Map<String, String> flatMap = new HashMap<>();
|
||||
|
||||
io.wispforest.accessories.api.AccessoriesCapability cap =
|
||||
io.wispforest.accessories.api.AccessoriesCapability.get(player);
|
||||
if (cap == null) {
|
||||
PlayerSync.LOGGER.debug("No Accessories capability for player {}", player.getUUID());
|
||||
return;
|
||||
}
|
||||
|
||||
Map<String, io.wispforest.accessories.api.AccessoriesContainer> containers = cap.getContainers();
|
||||
for (Map.Entry<String, io.wispforest.accessories.api.AccessoriesContainer> entry : containers.entrySet()) {
|
||||
String slotType = entry.getKey();
|
||||
io.wispforest.accessories.api.AccessoriesContainer container = entry.getValue();
|
||||
var accessories = container.getAccessories();
|
||||
for (int i = 0; i < accessories.getContainerSize(); i++) {
|
||||
ItemStack stack = accessories.getItem(i);
|
||||
if (!stack.isEmpty()) {
|
||||
flatMap.put(slotType + ":" + i, VanillaSync.getNbtForStorage(stack));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
String serializedData = flatMap.toString();
|
||||
JDBCsetUp.executePreparedUpdate(
|
||||
"REPLACE INTO mod_player_data (uuid, mod_id, data_value) VALUES (?, ?, ?)",
|
||||
player.getUUID().toString(), "accessories", serializedData);
|
||||
PlayerSync.LOGGER.debug("Saved Accessories data for player {}", player.getUUID());
|
||||
|
||||
} catch (Exception e) {
|
||||
PlayerSync.LOGGER.error("Error saving Accessories data for player {}", player.getUUID(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Restores Accessories inventory for a player.
|
||||
* Same logic as Curios restore: validate data before clearing, then restore items.
|
||||
*/
|
||||
public static void restoreAccessories(Player player) {
|
||||
if (!ModList.get().isLoaded("accessories")) return;
|
||||
|
||||
try {
|
||||
io.wispforest.accessories.api.AccessoriesCapability cap =
|
||||
io.wispforest.accessories.api.AccessoriesCapability.get(player);
|
||||
if (cap == null) {
|
||||
PlayerSync.LOGGER.debug("No Accessories capability for player {}", player.getUUID());
|
||||
return;
|
||||
}
|
||||
|
||||
String accessoriesData;
|
||||
try (JDBCsetUp.QueryResult qr = JDBCsetUp.executePreparedQuery(
|
||||
"SELECT data_value FROM mod_player_data WHERE uuid=? AND mod_id=?",
|
||||
player.getUUID().toString(), "accessories")) {
|
||||
ResultSet rs = qr.resultSet();
|
||||
if (!rs.next()) {
|
||||
// No data yet, perform initial save
|
||||
storeAccessories(player);
|
||||
return;
|
||||
}
|
||||
accessoriesData = rs.getString("data_value");
|
||||
}
|
||||
|
||||
// Validate data before clearing
|
||||
if (accessoriesData == null || accessoriesData.length() <= 2) {
|
||||
PlayerSync.LOGGER.debug("Empty Accessories data for player {}, skipping restore", player.getUUID());
|
||||
return;
|
||||
}
|
||||
|
||||
Map<String, String> storedMap = LocalJsonUtil.StringToMap(accessoriesData);
|
||||
if (storedMap.isEmpty()) return;
|
||||
|
||||
Map<String, io.wispforest.accessories.api.AccessoriesContainer> containers = cap.getContainers();
|
||||
|
||||
// Clear all Accessories slots ONLY after confirming valid data
|
||||
for (io.wispforest.accessories.api.AccessoriesContainer container : containers.values()) {
|
||||
var accessories = container.getAccessories();
|
||||
for (int i = 0; i < accessories.getContainerSize(); i++) {
|
||||
accessories.setItem(i, ItemStack.EMPTY);
|
||||
}
|
||||
}
|
||||
|
||||
// Restore items
|
||||
for (Map.Entry<String, String> entry : storedMap.entrySet()) {
|
||||
String compositeKey = entry.getKey();
|
||||
int lastColon = compositeKey.lastIndexOf(':');
|
||||
if (lastColon < 0) continue;
|
||||
|
||||
String slotType = compositeKey.substring(0, lastColon);
|
||||
int slotIndex;
|
||||
try {
|
||||
slotIndex = Integer.parseInt(compositeKey.substring(lastColon + 1));
|
||||
} catch (NumberFormatException ex) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
ItemStack stack = VanillaSync.deserializeAndCreatePlaceholderIfNeeded(entry.getValue());
|
||||
if (containers.containsKey(slotType)) {
|
||||
var accessories = containers.get(slotType).getAccessories();
|
||||
if (slotIndex < accessories.getContainerSize()) {
|
||||
accessories.setItem(slotIndex, stack);
|
||||
}
|
||||
}
|
||||
} catch (CommandSyntaxException e) {
|
||||
PlayerSync.LOGGER.error("Error deserializing Accessories data for key {}. Skipping.", compositeKey, e);
|
||||
} catch (Exception e) {
|
||||
PlayerSync.LOGGER.error("Unexpected error restoring Accessories data for key {}. Skipping.", compositeKey, e);
|
||||
}
|
||||
}
|
||||
|
||||
PlayerSync.LOGGER.info("Restored Accessories data for player {}", player.getUUID());
|
||||
} catch (Exception e) {
|
||||
PlayerSync.LOGGER.error("Error restoring Accessories data for player {}", player.getUUID(), e);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================
|
||||
// Cosmetic Armor Reworked
|
||||
// ============================
|
||||
|
||||
/**
|
||||
* Saves Cosmetic Armor slots (4 cosmetic equipment slots: head, chest, legs, feet).
|
||||
*/
|
||||
public static void storeCosmeticArmor(Player player) {
|
||||
if (!ModList.get().isLoaded("cosmeticarmorreworked")) return;
|
||||
|
||||
try {
|
||||
Map<Integer, String> flatMap = new HashMap<>();
|
||||
|
||||
lain.mods.cos.impl.inventory.InventoryCosArmor cosInv =
|
||||
lain.mods.cos.impl.ModObjects.invMan.getCosArmorInventory(player.getUUID());
|
||||
if (cosInv == null) {
|
||||
PlayerSync.LOGGER.debug("No CosmeticArmor inventory for player {}", player.getUUID());
|
||||
return;
|
||||
}
|
||||
|
||||
for (int i = 0; i < cosInv.getContainerSize(); i++) {
|
||||
ItemStack stack = cosInv.getItem(i);
|
||||
if (!stack.isEmpty()) {
|
||||
flatMap.put(i, VanillaSync.getNbtForStorage(stack));
|
||||
}
|
||||
}
|
||||
|
||||
String serializedData = flatMap.toString();
|
||||
JDBCsetUp.executePreparedUpdate(
|
||||
"REPLACE INTO mod_player_data (uuid, mod_id, data_value) VALUES (?, ?, ?)",
|
||||
player.getUUID().toString(), "cosmeticarmor", serializedData);
|
||||
PlayerSync.LOGGER.debug("Saved CosmeticArmor data for player {}", player.getUUID());
|
||||
|
||||
} catch (Exception e) {
|
||||
PlayerSync.LOGGER.error("Error saving CosmeticArmor data for player {}", player.getUUID(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Restores Cosmetic Armor slots for a player.
|
||||
*/
|
||||
public static void restoreCosmeticArmor(Player player) {
|
||||
if (!ModList.get().isLoaded("cosmeticarmorreworked")) return;
|
||||
|
||||
try {
|
||||
lain.mods.cos.impl.inventory.InventoryCosArmor cosInv =
|
||||
lain.mods.cos.impl.ModObjects.invMan.getCosArmorInventory(player.getUUID());
|
||||
if (cosInv == null) {
|
||||
PlayerSync.LOGGER.debug("No CosmeticArmor inventory for player {}", player.getUUID());
|
||||
return;
|
||||
}
|
||||
|
||||
String cosmeticData;
|
||||
try (JDBCsetUp.QueryResult qr = JDBCsetUp.executePreparedQuery(
|
||||
"SELECT data_value FROM mod_player_data WHERE uuid=? AND mod_id=?",
|
||||
player.getUUID().toString(), "cosmeticarmor")) {
|
||||
ResultSet rs = qr.resultSet();
|
||||
if (!rs.next()) {
|
||||
// No data yet, perform initial save
|
||||
storeCosmeticArmor(player);
|
||||
return;
|
||||
}
|
||||
cosmeticData = rs.getString("data_value");
|
||||
}
|
||||
|
||||
// Validate before clearing
|
||||
if (cosmeticData == null || cosmeticData.length() <= 2) {
|
||||
PlayerSync.LOGGER.debug("Empty CosmeticArmor data for player {}, skipping restore", player.getUUID());
|
||||
return;
|
||||
}
|
||||
|
||||
Map<Integer, String> storedMap = LocalJsonUtil.StringToEntryMap(cosmeticData);
|
||||
if (storedMap.isEmpty()) return;
|
||||
|
||||
// Clear cosmetic armor slots
|
||||
for (int i = 0; i < cosInv.getContainerSize(); i++) {
|
||||
cosInv.setItem(i, ItemStack.EMPTY);
|
||||
}
|
||||
|
||||
// Restore items
|
||||
for (Map.Entry<Integer, String> entry : storedMap.entrySet()) {
|
||||
int slot = entry.getKey();
|
||||
try {
|
||||
ItemStack stack = VanillaSync.deserializeAndCreatePlaceholderIfNeeded(entry.getValue());
|
||||
if (slot < cosInv.getContainerSize()) {
|
||||
cosInv.setItem(slot, stack);
|
||||
}
|
||||
} catch (CommandSyntaxException e) {
|
||||
PlayerSync.LOGGER.error("Error deserializing CosmeticArmor slot {}. Skipping.", slot, e);
|
||||
} catch (Exception e) {
|
||||
PlayerSync.LOGGER.error("Unexpected error restoring CosmeticArmor slot {}. Skipping.", slot, e);
|
||||
}
|
||||
}
|
||||
|
||||
// Mark the inventory as changed so the mod syncs to the client
|
||||
cosInv.setChanged();
|
||||
PlayerSync.LOGGER.info("Restored CosmeticArmor data for player {}", player.getUUID());
|
||||
|
||||
} catch (Exception e) {
|
||||
PlayerSync.LOGGER.error("Error restoring CosmeticArmor data for player {}", player.getUUID(), e);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================
|
||||
// Convenience methods
|
||||
// ============================
|
||||
|
||||
/**
|
||||
* Saves all mod-specific data for a player.
|
||||
* Called on logout and auto-save.
|
||||
*/
|
||||
public static void storeAll(Player player) {
|
||||
storeAccessories(player);
|
||||
storeCosmeticArmor(player);
|
||||
}
|
||||
|
||||
/**
|
||||
* Restores all mod-specific data for a player.
|
||||
* Called on join.
|
||||
*/
|
||||
public static void restoreAll(Player player) {
|
||||
restoreAccessories(player);
|
||||
restoreCosmeticArmor(player);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user