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:
laforetbrut 2026-03-26 11:21:09 +01:00
parent 03b57c3e6b
commit c63d5849a3
4 changed files with 311 additions and 0 deletions

View File

@ -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"

View File

@ -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) {

View File

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

View File

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