diff --git a/build.gradle b/build.gradle index 8722f81..d65ee33 100644 --- a/build.gradle +++ b/build.gradle @@ -18,22 +18,10 @@ repositories { // for more information about repositories. } -loom { - splitEnvironmentSourceSets() - - mods { - "statssync" { - sourceSet sourceSets.main - sourceSet sourceSets.client - } - } - -} - dependencies { // To change the versions see the gradle.properties file minecraft "com.mojang:minecraft:${project.minecraft_version}" - mappings "net.fabricmc:yarn:${project.yarn_mappings}:v2" + mappings loom.officialMojangMappings() modImplementation "net.fabricmc:fabric-loader:${project.loader_version}" // Fabric API. This is technically optional, but you probably want it anyway. diff --git a/src/client/java/com/yusuf007r/statssync/StatsSyncClient.java b/src/client/java/com/yusuf007r/statssync/StatsSyncClient.java deleted file mode 100644 index 957d53f..0000000 --- a/src/client/java/com/yusuf007r/statssync/StatsSyncClient.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.yusuf007r.statssync; - -import net.fabricmc.api.ClientModInitializer; - -public class StatsSyncClient implements ClientModInitializer { - @Override - public void onInitializeClient() { - // This entrypoint is suitable for setting up client-specific logic, such as rendering. - } -} \ No newline at end of file diff --git a/src/client/java/com/yusuf007r/statssync/mixin/client/ExampleClientMixin.java b/src/client/java/com/yusuf007r/statssync/mixin/client/ExampleClientMixin.java deleted file mode 100644 index 4fba89b..0000000 --- a/src/client/java/com/yusuf007r/statssync/mixin/client/ExampleClientMixin.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.yusuf007r.statssync.mixin.client; - -import net.minecraft.client.MinecraftClient; -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.injection.At; -import org.spongepowered.asm.mixin.injection.Inject; -import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; - -@Mixin(MinecraftClient.class) -public class ExampleClientMixin { - @Inject(at = @At("HEAD"), method = "run") - private void init(CallbackInfo info) { - // This code is injected into the start of MinecraftClient.run()V - } -} \ No newline at end of file diff --git a/src/client/resources/statssync.client.mixins.json b/src/client/resources/statssync.client.mixins.json deleted file mode 100644 index 97fc9d3..0000000 --- a/src/client/resources/statssync.client.mixins.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "required": true, - "package": "com.yusuf007r.statssync.mixin.client", - "compatibilityLevel": "JAVA_21", - "client": [ - "ExampleClientMixin" - ], - "injectors": { - "defaultRequire": 1 - } -} \ No newline at end of file diff --git a/src/main/java/com/yusuf007r/statssync/StatsSync.java b/src/main/java/com/yusuf007r/statssync/StatsSync.java index 7c98857..c00ba97 100644 --- a/src/main/java/com/yusuf007r/statssync/StatsSync.java +++ b/src/main/java/com/yusuf007r/statssync/StatsSync.java @@ -1,99 +1,29 @@ package com.yusuf007r.statssync; -import static net.minecraft.server.command.CommandManager.*; - -import com.mojang.authlib.GameProfile; +import com.mojang.brigadier.arguments.StringArgumentType; import net.fabricmc.api.ModInitializer; import net.fabricmc.fabric.api.command.v2.CommandRegistrationCallback; -import net.minecraft.command.argument.ScoreboardCriterionArgumentType; -import net.minecraft.command.argument.ScoreboardObjectiveArgumentType; -import net.minecraft.scoreboard.*; -import net.minecraft.server.MinecraftServer; -import net.minecraft.server.command.CommandManager; -import net.minecraft.server.command.ServerCommandSource; -import net.minecraft.stat.ServerStatHandler; -import net.minecraft.stat.Stat; -import net.minecraft.text.Text; -import net.minecraft.util.UserCache; -import net.minecraft.util.WorldSavePath; -import org.jetbrains.annotations.NotNull; +import net.minecraft.commands.arguments.ObjectiveArgument; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.io.File; -import java.nio.file.Path; -import java.util.Objects; -import java.util.Optional; -import java.util.UUID; +import static net.minecraft.commands.Commands.argument; +import static net.minecraft.commands.Commands.literal; +import static net.minecraft.commands.SharedSuggestionProvider.suggest; + public class StatsSync implements ModInitializer { - public static final String MOD_ID = "statssync"; + public static final String MOD_ID = "stats-sync"; public static final Logger LOGGER = LoggerFactory.getLogger(MOD_ID); + public static final UserCacheManager userCacheManager = new UserCacheManager(); - void updateObjective(MinecraftServer server, ScoreboardObjective objective, ScoreHolder scoreHolder) { - ScoreboardCriterion criterion = objective.getCriterion(); - Optional<UUID> playerUUID = getPlayerUUID(server, scoreHolder); - - if (playerUUID.isEmpty()) { - LOGGER.warn("Could not find UUID for player: {}", scoreHolder.getNameForScoreboard()); - return; - } - - Path statsPath = getStatsFilePath(server, playerUUID.get()); - if (!statsPath.toFile().exists()) { - LOGGER.warn("Stats file not found for player: {}", scoreHolder.getNameForScoreboard()); - return; - } - - int statValue = getStatValue(server, statsPath, criterion); - if (statValue > 0) { - updateScore(server, objective, scoreHolder, statValue); - } - } - - private Optional<UUID> getPlayerUUID(@NotNull MinecraftServer server, ScoreHolder scoreHolder) { - return Optional.ofNullable(server.getUserCache()).flatMap(cache -> cache.findByName(scoreHolder.getNameForScoreboard())).map(GameProfile::getId); - } - - private @NotNull Path getStatsFilePath(@NotNull MinecraftServer server, UUID playerUUID) { - return server.getSavePath(WorldSavePath.STATS).resolve(playerUUID + ".json"); - } - private int getStatValue(MinecraftServer server, @NotNull Path statsPath, ScoreboardCriterion criterion) { - ServerStatHandler statHandler = new ServerStatHandler(server, statsPath.toFile()); - return statHandler.getStat((Stat<?>) criterion); - } - - private void updateScore(@NotNull MinecraftServer server, ScoreboardObjective objective, ScoreHolder scoreHolder, int value) { - server.getScoreboard().getOrCreateScore(scoreHolder, objective).setScore(value); - } - - - @Override public void onInitialize() { - LOGGER.info("Hello Fabric world!"); - - CommandRegistrationCallback.EVENT.register((dispatcher, registryAccess, environment) -> dispatcher.register(literal("statSync").executes(context -> { - ServerCommandSource source = context.getSource(); - MinecraftServer server = source.getServer(); - ServerScoreboard scoreboard = server.getScoreboard(); - - - source.sendFeedback(()-> Text.literal("Found " + scoreboard.getObjectives().size() + " Objectives, will try updating them with player stats"), false); - scoreboard.getObjectives().forEach((obj) -> { - try { - source.sendFeedback(()-> Text.literal("trying to update: " + obj.getDisplayName().getString()), false); - scoreboard.getKnownScoreHolders().forEach((scoreHolder -> { - updateObjective(server, obj, scoreHolder); - })); - - } catch (Exception e) { - LOGGER.info("Error while updating {}, {} the error was {}", obj.getDisplayName().getString(), obj.getCriterion().getName(), e.getMessage()); - source.sendFeedback(()-> Text.literal("Error while updating: " + obj.getDisplayName().getString()), false); - } - }); - return 1; - }))); + CommandRegistrationCallback.EVENT.register((dispatcher, registryAccess, environment) -> dispatcher.register(literal("statSync").executes((c) -> new StatsSyncCommand(c).executeAllObjectivesAllPlayers()) + .then(argument("objective", new ObjectiveArgument()).executes(c -> new StatsSyncCommand(c).executeSpecificObjectiveAllPlayers()) + .then(argument("player", StringArgumentType.word()).suggests((c, b) -> suggest(userCacheManager.getUserCache() + .stream().map(user -> user.profile().getName()), b)) + .executes(c -> new StatsSyncCommand(c).executeSpecificObjectiveSpecificPlayer()))))); } } \ No newline at end of file diff --git a/src/main/java/com/yusuf007r/statssync/StatsSyncCommand.java b/src/main/java/com/yusuf007r/statssync/StatsSyncCommand.java new file mode 100644 index 0000000..46a192c --- /dev/null +++ b/src/main/java/com/yusuf007r/statssync/StatsSyncCommand.java @@ -0,0 +1,120 @@ +package com.yusuf007r.statssync; + +import com.mojang.authlib.GameProfile; +import com.mojang.brigadier.arguments.StringArgumentType; +import com.mojang.brigadier.context.CommandContext; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.commands.arguments.ObjectiveArgument; +import net.minecraft.network.chat.Component; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.ServerScoreboard; +import net.minecraft.stats.ServerStatsCounter; +import net.minecraft.stats.Stat; +import net.minecraft.world.level.storage.LevelResource; +import net.minecraft.world.scores.Objective; +import net.minecraft.world.scores.ScoreAccess; +import net.minecraft.world.scores.ScoreHolder; +import net.minecraft.world.scores.criteria.ObjectiveCriteria; +import org.jetbrains.annotations.NotNull; +import org.slf4j.Logger; + +import java.nio.file.Path; +import java.util.Optional; +import java.util.UUID; + +public class StatsSyncCommand { + + final private CommandSourceStack source; + final private MinecraftServer server; + final private ServerScoreboard scoreboard; + final private CommandContext<CommandSourceStack> context; + final private UserCacheManager userCacheManager = StatsSync.userCacheManager; + public static final Logger LOGGER = StatsSync.LOGGER; + + + StatsSyncCommand(CommandContext<CommandSourceStack> context) { + this.context = context; + this.source = context.getSource(); + this.server = source.getServer(); + this.scoreboard = server.getScoreboard(); + + } + + + public int executeAllObjectivesAllPlayers() { + source.sendSuccess(() -> Component.literal("Updating all objectives for all players"), false); + scoreboard.getObjectives().forEach(this::updateObjectiveForAllPlayers); + + return 1; + } + + public int executeSpecificObjectiveAllPlayers() throws CommandSyntaxException { + Objective objective = ObjectiveArgument.getObjective(context, "objective"); + source.sendSuccess(() -> Component.literal("Updating objective " + objective.getDisplayName() + .getString() + " for all players"), false); + updateObjectiveForAllPlayers(objective); + + return 1; + } + + public int executeSpecificObjectiveSpecificPlayer() throws CommandSyntaxException { + Objective objective = ObjectiveArgument.getObjective(context, "objective"); + String playerName = StringArgumentType.getString(context, "player"); + Optional<UserCacheManager.Entry> entry = userCacheManager.getUserByName(playerName); + if (entry.isEmpty()) { + LOGGER.error("couldn't find user {} in the cache", playerName); + } + source.sendSuccess(() -> Component.literal("Updating objective " + objective.getDisplayName() + .getString() + " for player " + entry.orElseThrow().profile().getName()), false); + updateObjective(objective, entry.orElseThrow().profile()); + + return 1; + } + + + private void updateObjectiveForAllPlayers(Objective objective) { + try { + source.sendSuccess(() -> Component.literal("Updating: " + objective.getDisplayName().getString()), false); + for (UserCacheManager.Entry entry : userCacheManager.getUserCache()) { + updateObjective(objective, entry.profile()); + } + } catch (Exception e) { + LOGGER.error("Error while updating {}: {}", objective.getDisplayName().getString(), e.getMessage()); + source.sendSuccess(() -> Component.literal("Error while updating: " + objective.getDisplayName() + .getString()), false); + } + } + + void updateObjective(@NotNull Objective objective, GameProfile profile) { + ObjectiveCriteria criterion = objective.getCriteria(); + UUID playerUUID = profile.getId(); + + Path statsPath = getStatsFilePath(playerUUID); + if (!statsPath.toFile().exists()) { + LOGGER.warn("Stats file not found for player: {}", profile.getName()); + return; + } + + int statValue = getStatValue(statsPath, criterion); + if (statValue > 0) { + updateScore(objective, profile, statValue); + } + } + + + private @NotNull Path getStatsFilePath(UUID playerUUID) { + return server.getWorldPath(LevelResource.PLAYER_STATS_DIR).resolve(playerUUID + ".json"); + } + + private int getStatValue(@NotNull Path statsPath, ObjectiveCriteria criterion) { + ServerStatsCounter statHandler = new ServerStatsCounter(server, statsPath.toFile()); + return statHandler.getValue((Stat<?>) criterion); + } + + private void updateScore(Objective objective, GameProfile profile, int value) { + ScoreAccess scoreAccess = scoreboard.getOrCreatePlayerScore(ScoreHolder.fromGameProfile(profile), objective); + scoreAccess.set(value); + } + +} diff --git a/src/main/java/com/yusuf007r/statssync/UserCacheManager.java b/src/main/java/com/yusuf007r/statssync/UserCacheManager.java new file mode 100644 index 0000000..7cce618 --- /dev/null +++ b/src/main/java/com/yusuf007r/statssync/UserCacheManager.java @@ -0,0 +1,102 @@ +package com.yusuf007r.statssync; + +import com.google.common.collect.Maps; +import com.google.gson.*; +import com.mojang.authlib.GameProfile; +import net.minecraft.util.StringUtil; +import org.slf4j.Logger; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.text.DateFormat; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.*; + +public class UserCacheManager { + private final Map<String, Entry> byName = Maps.newHashMap(); + public static final Logger LOGGER = StatsSync.LOGGER; + + public record Entry(GameProfile profile, Date expirationDate) { + + } + + public UserCacheManager() { + this.init(); + } + + + public Collection<Entry> getUserCache() { + return byName.values(); + } + + public Optional<Entry> getUserByName(String name) { + return Optional.ofNullable(byName.get(name)); + } + + private void add(Entry entry) { + if (!StringUtil.isValidPlayerName(entry.profile.getName())) return; + if ((byName.containsKey(entry.profile.getName()) && byName.get(entry.profile.getName()).expirationDate.after(entry.expirationDate))) + return; + byName.put(entry.profile.getName(), entry); + } + + private void init() { + try { + Path path = Paths.get("./usercache.json"); + if (Files.exists(path)) { + String content = new String(Files.readAllBytes(path)); + JsonElement jsonElement = JsonParser.parseString(content); + if (jsonElement.isJsonArray()) { + jsonElement.getAsJsonArray().forEach(json -> processJson(json).ifPresent(this::add)); + } + + } + } catch (IOException e) { + LOGGER.error(e.getMessage()); + } + + } + + private Optional<Entry> processJson(JsonElement json) { + DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss Z", Locale.ROOT); + if (json.isJsonObject()) { + JsonObject jsonObject = json.getAsJsonObject(); + JsonElement jsonElement = jsonObject.get("name"); + JsonElement jsonElement2 = jsonObject.get("uuid"); + JsonElement jsonElement3 = jsonObject.get("expiresOn"); + if (jsonElement != null && jsonElement2 != null) { + String string = jsonElement2.getAsString(); + String string2 = jsonElement.getAsString(); + Date date = null; + if (jsonElement3 != null) { + try { + date = dateFormat.parse(jsonElement3.getAsString()); + } catch (ParseException var12) { + LOGGER.error(var12.getMessage()); + } + } + + if (string2 != null && string != null && date != null) { + UUID uUID; + try { + uUID = UUID.fromString(string); + } catch (Throwable var11) { + return Optional.empty(); + } + + GameProfile gameProfile = new GameProfile(uUID, string2); + return Optional.of(new Entry(gameProfile, date)); + } else { + return Optional.empty(); + } + } else { + return Optional.empty(); + } + } else { + return Optional.empty(); + } + } +} diff --git a/src/main/java/com/yusuf007r/statssync/mixin/ExampleMixin.java b/src/main/java/com/yusuf007r/statssync/mixin/ExampleMixin.java deleted file mode 100644 index b3b0eef..0000000 --- a/src/main/java/com/yusuf007r/statssync/mixin/ExampleMixin.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.yusuf007r.statssync.mixin; - -import net.minecraft.server.MinecraftServer; -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.injection.At; -import org.spongepowered.asm.mixin.injection.Inject; -import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; - -@Mixin(MinecraftServer.class) -public class ExampleMixin { - @Inject(at = @At("HEAD"), method = "loadWorld") - private void init(CallbackInfo info) { - // This code is injected into the start of MinecraftServer.loadWorld()V - } -} \ No newline at end of file diff --git a/src/main/resources/fabric.mod.json b/src/main/resources/fabric.mod.json index d0c9650..b015231 100644 --- a/src/main/resources/fabric.mod.json +++ b/src/main/resources/fabric.mod.json @@ -17,17 +17,9 @@ "entrypoints": { "main": [ "com.yusuf007r.statssync.StatsSync" - ], - "client": [ - "com.yusuf007r.statssync.StatsSyncClient" ] }, "mixins": [ - "statssync.mixins.json", - { - "config": "statssync.client.mixins.json", - "environment": "client" - } ], "depends": { "fabricloader": ">=0.15.11", diff --git a/src/main/resources/statssync.mixins.json b/src/main/resources/statssync.mixins.json deleted file mode 100644 index 54fa40a..0000000 --- a/src/main/resources/statssync.mixins.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "required": true, - "package": "com.yusuf007r.statssync.mixin", - "compatibilityLevel": "JAVA_21", - "mixins": [ - "ExampleMixin" - ], - "injectors": { - "defaultRequire": 1 - } -} \ No newline at end of file