A Minecraft Fabric mod that connects the game with ATProtocol ⛏️
8
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: add server-side sync preferences persistence

- Implement PlayerSyncPreferences data class (UUID stored as String for serialization)
- Store player preferences per-player in JSON files
- Methods to load, save, update, and delete player preferences
- Support for granular sync control: stats, sessions, achievements, server status
- Track sync frequencies for each data type
- Admin methods to list all players with preferences
- Automatic directory creation and error handling

+188
+188
src/main/kotlin/com/jollywhoppers/atproto/server/PlayerSyncPreferencesStore.kt
··· 1 + package com.jollywhoppers.atproto.server 2 + 3 + import kotlinx.serialization.Serializable 4 + import kotlinx.serialization.json.Json 5 + import net.fabricmc.loader.api.FabricLoader 6 + import org.slf4j.LoggerFactory 7 + import java.nio.file.Files 8 + import java.util.UUID 9 + import kotlin.io.path.exists 10 + import kotlin.io.path.readText 11 + import kotlin.io.path.writeText 12 + 13 + /** 14 + * Server-side sync preferences for players. 15 + * Stores what data each player has consented to sync. 16 + * 17 + * These are the server's record of player consent, persisted to disk. 18 + * The client has its own local copy; the server respects the player's choices. 19 + */ 20 + @Serializable 21 + data class PlayerSyncPreferences( 22 + val playerId: String, // Store UUID as String for serialization 23 + val syncStatsEnabled: Boolean = true, 24 + val syncSessionsEnabled: Boolean = true, 25 + val syncAchievementsEnabled: Boolean = true, 26 + val syncServerStatusEnabled: Boolean = false, 27 + val statsSyncFrequency: Int = 60, // minutes 28 + val sessionSyncFrequency: Int = 5, // minutes 29 + val achievementSyncFrequency: Int = 30, // minutes 30 + val lastUpdated: Long = System.currentTimeMillis(), 31 + ) { 32 + /** 33 + * Check if any sync is enabled 34 + */ 35 + fun isAnySyncEnabled(): Boolean { 36 + return syncStatsEnabled || syncSessionsEnabled || syncAchievementsEnabled || syncServerStatusEnabled 37 + } 38 + 39 + /** 40 + * Check if a specific data type should be synced 41 + */ 42 + fun shouldSync(dataType: String): Boolean = when (dataType) { 43 + "stats" -> syncStatsEnabled 44 + "sessions" -> syncSessionsEnabled 45 + "achievements" -> syncAchievementsEnabled 46 + "server_status" -> syncServerStatusEnabled 47 + else -> false 48 + } 49 + 50 + /** 51 + * Get sync frequency for a data type (in minutes) 52 + */ 53 + fun getSyncFrequency(dataType: String): Int = when (dataType) { 54 + "stats" -> statsSyncFrequency 55 + "sessions" -> sessionSyncFrequency 56 + "achievements" -> achievementSyncFrequency 57 + else -> 60 58 + } 59 + } 60 + 61 + /** 62 + * Server-side persistent storage for player sync preferences. 63 + * Each player's preferences are stored in a JSON file. 64 + */ 65 + object PlayerSyncPreferencesStore { 66 + private val logger = LoggerFactory.getLogger("atproto-connect-server") 67 + private val json = Json { prettyPrint = true } 68 + private val preferencesDir: java.nio.file.Path by lazy { 69 + FabricLoader.getInstance().configDir.resolve("atproto-connect").resolve("player-preferences") 70 + } 71 + 72 + init { 73 + try { 74 + Files.createDirectories(preferencesDir) 75 + logger.info("Initialized player sync preferences directory") 76 + } catch (e: Exception) { 77 + logger.error("Failed to create preferences directory: ${e.message}", e) 78 + } 79 + } 80 + 81 + /** 82 + * Get preferences file path for a player 83 + */ 84 + private fun getPreferencesFile(playerId: UUID) = 85 + preferencesDir.resolve("${playerId}.json") 86 + 87 + /** 88 + * Load or create default preferences for a player 89 + */ 90 + fun getOrDefault(playerId: UUID): PlayerSyncPreferences { 91 + return try { 92 + val file = getPreferencesFile(playerId) 93 + if (file.exists()) { 94 + json.decodeFromString<PlayerSyncPreferences>(file.readText()).also { 95 + logger.debug("Loaded sync preferences for player $playerId") 96 + } 97 + } else { 98 + PlayerSyncPreferences(playerId = playerId.toString()).also { 99 + logger.debug("Created default sync preferences for new player $playerId") 100 + } 101 + } 102 + } catch (e: Exception) { 103 + logger.error("Failed to load sync preferences for $playerId: ${e.message}", e) 104 + PlayerSyncPreferences(playerId = playerId.toString()) 105 + } 106 + } 107 + 108 + /** 109 + * Save player sync preferences to disk 110 + */ 111 + fun save(preferences: PlayerSyncPreferences) { 112 + try { 113 + Files.createDirectories(preferencesDir) 114 + val file = preferencesDir.resolve("${preferences.playerId}.json") 115 + val content = json.encodeToString(PlayerSyncPreferences.serializer(), preferences) 116 + file.writeText(content) 117 + logger.debug("Saved sync preferences for player ${preferences.playerId}") 118 + } catch (e: Exception) { 119 + logger.error("Failed to save sync preferences for ${preferences.playerId}: ${e.message}", e) 120 + } 121 + } 122 + 123 + /** 124 + * Update sync preferences for a player 125 + */ 126 + fun update( 127 + playerId: UUID, 128 + stats: Boolean? = null, 129 + sessions: Boolean? = null, 130 + achievements: Boolean? = null, 131 + serverStatus: Boolean? = null, 132 + statsFrequency: Int? = null, 133 + sessionsFrequency: Int? = null, 134 + achievementsFrequency: Int? = null, 135 + ) { 136 + val current = getOrDefault(playerId) 137 + val updated = current.copy( 138 + syncStatsEnabled = stats ?: current.syncStatsEnabled, 139 + syncSessionsEnabled = sessions ?: current.syncSessionsEnabled, 140 + syncAchievementsEnabled = achievements ?: current.syncAchievementsEnabled, 141 + syncServerStatusEnabled = serverStatus ?: current.syncServerStatusEnabled, 142 + statsSyncFrequency = statsFrequency ?: current.statsSyncFrequency, 143 + sessionSyncFrequency = sessionsFrequency ?: current.sessionSyncFrequency, 144 + achievementSyncFrequency = achievementsFrequency ?: current.achievementSyncFrequency, 145 + lastUpdated = System.currentTimeMillis(), 146 + ) 147 + save(updated) 148 + } 149 + 150 + /** 151 + * Delete preferences for a player (on unlink/account deletion) 152 + */ 153 + fun delete(playerId: UUID) { 154 + try { 155 + val file = getPreferencesFile(playerId) 156 + if (file.exists()) { 157 + Files.delete(file) 158 + logger.info("Deleted sync preferences for player $playerId") 159 + } 160 + } catch (e: Exception) { 161 + logger.error("Failed to delete sync preferences for $playerId: ${e.message}", e) 162 + } 163 + } 164 + 165 + /** 166 + * Get all players with preferences (for admin operations) 167 + */ 168 + fun getAllPlayerIds(): List<UUID> { 169 + return try { 170 + Files.list(preferencesDir).use { stream -> 171 + stream 172 + .filter { it.fileName.toString().endsWith(".json") } 173 + .toList() 174 + .mapNotNull { file -> 175 + try { 176 + UUID.fromString(file.fileName.toString().removeSuffix(".json")) 177 + } catch (e: Exception) { 178 + logger.warn("Invalid preferences file: ${file.fileName}") 179 + null 180 + } 181 + } 182 + } 183 + } catch (e: Exception) { 184 + logger.error("Failed to list player preferences: ${e.message}", e) 185 + emptyList() 186 + } 187 + } 188 + }