1
0
mirror of https://github.com/Suiranoil/SkinRestorer.git synced 2026-01-16 04:42:12 +00:00

Merge pull request #4 from CaveNightingale/master

Fix the bug that performing http transform on the main thread & Allow player to apply their skin changes immediately
This commit is contained in:
Suiranoil
2022-11-18 14:34:28 +03:00
committed by GitHub
10 changed files with 284 additions and 51 deletions

View File

@@ -16,6 +16,7 @@ repositories {
// Loom adds the essential maven repositories to download Minecraft and libraries from automatically.
// See https://docs.gradle.org/current/userguide/declaring_repositories.html
// for more information about repositories.
maven { url = "https://masa.dy.fi/maven" }
}
dependencies {
@@ -26,6 +27,7 @@ dependencies {
// PSA: Some older mods, compiled on Loom 0.2.1, might have outdated Maven POMs.
// You may need to force-disable transitiveness on them.
modImplementation "carpet:fabric-carpet:${project.carpet_branch}-${project.carpet_core_version}"
}
processResources {

View File

@@ -9,3 +9,6 @@ loader_version=0.14.7
mod_version=1.1.0
maven_group=net.lionarius
archives_base_name=skin-restorer
# Fabric Carpet
carpet_branch=1.19
carpet_core_version=1.4.79+v220607

View File

@@ -1,12 +1,35 @@
package net.lionarius.skinrestorer;
import com.google.gson.Gson;
import com.google.gson.JsonObject;
import com.mojang.authlib.GameProfile;
import com.mojang.authlib.properties.Property;
import it.unimi.dsi.fastutil.Pair;
import net.fabricmc.api.DedicatedServerModInitializer;
import net.fabricmc.loader.api.FabricLoader;
import net.minecraft.entity.effect.StatusEffectInstance;
import net.minecraft.entity.player.PlayerEntity;
import net.minecraft.network.packet.s2c.play.*;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.network.ServerPlayerEntity;
import net.minecraft.util.math.GlobalPos;
import net.minecraft.world.biome.source.BiomeAccess;
import org.jetbrains.annotations.NotNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import java.util.function.Supplier;
public class SkinRestorer implements DedicatedServerModInitializer {
private static SkinStorage skinStorage;
public static final Logger LOGGER = LoggerFactory.getLogger("SkinRestorer");
public static SkinStorage getSkinStorage() {
return skinStorage;
}
@@ -15,4 +38,99 @@ public class SkinRestorer implements DedicatedServerModInitializer {
public void onInitializeServer() {
skinStorage = new SkinStorage(new SkinIO(FabricLoader.getInstance().getConfigDir().resolve("skinrestorer")));
}
public static CompletableFuture<Pair<Collection<ServerPlayerEntity>, Collection<GameProfile>>> setSkinAsync(MinecraftServer server, Collection<GameProfile> targets, Supplier<Property> skinSupplier) {
return CompletableFuture.<Pair<Property, Collection<GameProfile>>>supplyAsync(() -> {
HashSet<GameProfile> acceptedProfiles = new HashSet<>();
Property skin = skinSupplier.get();
if (skin == null) {
SkinRestorer.LOGGER.error("Cannot get the skin for {}", targets.stream().findFirst().orElseThrow());
return Pair.of(null, Collections.emptySet());
}
for (GameProfile profile : targets) {
SkinRestorer.getSkinStorage().setSkin(profile.getId(), skin);
acceptedProfiles.add(profile);
}
return Pair.of(skin, acceptedProfiles);
}).<Pair<Collection<ServerPlayerEntity>, Collection<GameProfile>>>thenApplyAsync(pair -> {
Property skin = pair.left();
if (skin == null) {
return Pair.of(Collections.emptySet(), Collections.emptySet());
}
Collection<GameProfile> acceptedProfiles = pair.right();
HashSet<ServerPlayerEntity> acceptedPlayers = new HashSet<>();
JsonObject newSkinJson = gson.fromJson(new String(Base64.getDecoder().decode(skin.getValue()), StandardCharsets.UTF_8), JsonObject.class);
newSkinJson.remove("timestamp");
for (GameProfile profile : acceptedProfiles) {
ServerPlayerEntity player = server.getPlayerManager().getPlayer(profile.getId());
if (player == null) {
continue;
}
if (arePropertiesEquals(newSkinJson, player.getGameProfile())) {
continue;
}
applyRestoredSkin(player, skin);
for (PlayerEntity observer : player.world.getPlayers()) {
ServerPlayerEntity observer1 = (ServerPlayerEntity) observer;
observer1.networkHandler.sendPacket(new PlayerListS2CPacket(PlayerListS2CPacket.Action.REMOVE_PLAYER, player));
observer1.networkHandler.sendPacket(new PlayerListS2CPacket(PlayerListS2CPacket.Action.ADD_PLAYER, player)); // refresh the player information
if (player != observer1 && observer1.canSee(player)) {
observer1.networkHandler.sendPacket(new EntitiesDestroyS2CPacket(player.getId()));
observer1.networkHandler.sendPacket(new PlayerSpawnS2CPacket(player));
observer1.networkHandler.sendPacket(new EntityTrackerUpdateS2CPacket(player.getId(), player.getDataTracker(), true));
observer1.networkHandler.sendPacket(new EntityPositionS2CPacket(player));
} else if (player == observer1) {
observer1.networkHandler.sendPacket(new PlayerRespawnS2CPacket(
observer1.world.getDimensionKey(),
observer1.world.getRegistryKey(),
BiomeAccess.hashSeed(observer1.getWorld().getSeed()),
observer1.interactionManager.getGameMode(),
observer1.interactionManager.getPreviousGameMode(),
observer1.getWorld().isDebugWorld(),
observer1.getWorld().isFlat(),
true,
Optional.of(GlobalPos.create(observer1.getWorld().getRegistryKey(), observer1.getBlockPos()))
));
observer1.requestTeleport(observer1.getX(), observer1.getY(), observer1.getZ());
observer1.networkHandler.sendPacket(new UpdateSelectedSlotS2CPacket(observer1.getInventory().selectedSlot));
observer1.sendAbilitiesUpdate();
observer1.playerScreenHandler.updateToClient();
for (StatusEffectInstance instance : observer1.getStatusEffects()) {
observer1.networkHandler.sendPacket(new EntityStatusEffectS2CPacket(observer1.getId(), instance));
}
observer1.networkHandler.sendPacket(new EntityTrackerUpdateS2CPacket(player.getId(), player.getDataTracker(), true));
observer1.networkHandler.sendPacket(new ExperienceBarUpdateS2CPacket(player.experienceProgress, player.totalExperience, player.experienceLevel));
}
}
acceptedPlayers.add(player);
}
return Pair.of(acceptedPlayers, acceptedProfiles);
}, server).orTimeout(10, TimeUnit.SECONDS).exceptionally(e -> Pair.of(Collections.emptySet(), Collections.emptySet()));
}
private static void applyRestoredSkin(ServerPlayerEntity playerEntity, Property skin) {
playerEntity.getGameProfile().getProperties().removeAll("textures");
playerEntity.getGameProfile().getProperties().put("textures", skin);
}
private static final Gson gson = new Gson();
private static boolean arePropertiesEquals(@NotNull JsonObject x, @NotNull GameProfile y) {
Property py = y.getProperties().get("textures").stream().findFirst().orElse(null);
if (py == null) {
return false;
} else {
try {
JsonObject jy = gson.fromJson(new String(Base64.getDecoder().decode(py.getValue()), StandardCharsets.UTF_8), JsonObject.class);
jy.remove("timestamp");
return x.equals(jy);
} catch (Exception ex) {
SkinRestorer.LOGGER.info("Can not compare skin", ex);
return false;
}
}
}
}

View File

@@ -1,5 +1,6 @@
package net.lionarius.skinrestorer.command;
import com.mojang.authlib.GameProfile;
import com.mojang.authlib.properties.Property;
import com.mojang.brigadier.CommandDispatcher;
import com.mojang.brigadier.arguments.StringArgumentType;
@@ -7,7 +8,8 @@ import net.lionarius.skinrestorer.MineskinSkinProvider;
import net.lionarius.skinrestorer.MojangSkinProvider;
import net.lionarius.skinrestorer.SkinRestorer;
import net.lionarius.skinrestorer.enums.SkinVariant;
import net.minecraft.command.argument.EntityArgumentType;
import net.lionarius.skinrestorer.util.TranslationUtils;
import net.minecraft.command.argument.GameProfileArgumentType;
import net.minecraft.server.command.ServerCommandSource;
import net.minecraft.server.network.ServerPlayerEntity;
import net.minecraft.text.Text;
@@ -16,6 +18,7 @@ import java.util.Collection;
import java.util.Collections;
import java.util.function.Supplier;
import static net.lionarius.skinrestorer.SkinStorage.DEFAULT_SKIN;
import static net.minecraft.server.command.CommandManager.argument;
import static net.minecraft.server.command.CommandManager.literal;
@@ -27,58 +30,71 @@ public class SkinCommand {
.then(literal("mojang")
.then(argument("skin_name", StringArgumentType.word())
.executes(context ->
skinAction(Collections.singleton(context.getSource().getPlayer()), false,
skinAction(context.getSource(), false,
() -> MojangSkinProvider.getSkin(StringArgumentType.getString(context, "skin_name"))))
.then(argument("targets", EntityArgumentType.players()).requires(source -> source.hasPermissionLevel(3))
.then(argument("targets", GameProfileArgumentType.gameProfile()).requires(source -> source.hasPermissionLevel(3))
.executes(context ->
skinAction(EntityArgumentType.getPlayers(context, "targets"), true,
skinAction(context.getSource(), GameProfileArgumentType.getProfileArgument(context, "targets"), true,
() -> MojangSkinProvider.getSkin(StringArgumentType.getString(context, "skin_name")))))))
.then(literal("web")
.then(literal("classic")
.then(argument("url", StringArgumentType.string())
.executes(context ->
skinAction(Collections.singleton(context.getSource().getPlayer()), false,
skinAction(context.getSource(), false,
() -> MineskinSkinProvider.getSkin(StringArgumentType.getString(context, "url"), SkinVariant.CLASSIC)))
.then(argument("targets", EntityArgumentType.players()).requires(source -> source.hasPermissionLevel(3))
.then(argument("targets", GameProfileArgumentType.gameProfile()).requires(source -> source.hasPermissionLevel(3))
.executes(context ->
skinAction(EntityArgumentType.getPlayers(context, "targets"), true,
skinAction(context.getSource(), GameProfileArgumentType.getProfileArgument(context, "targets"), true,
() -> MineskinSkinProvider.getSkin(StringArgumentType.getString(context, "url"), SkinVariant.CLASSIC))))))
.then(literal("slim")
.then(argument("url", StringArgumentType.string())
.executes(context ->
skinAction(Collections.singleton(context.getSource().getPlayer()), false,
skinAction(context.getSource(), false,
() -> MineskinSkinProvider.getSkin(StringArgumentType.getString(context, "url"), SkinVariant.SLIM)))
.then(argument("targets", EntityArgumentType.players()).requires(source -> source.hasPermissionLevel(3))
.then(argument("targets", GameProfileArgumentType.gameProfile()).requires(source -> source.hasPermissionLevel(3))
.executes(context ->
skinAction(EntityArgumentType.getPlayers(context, "targets"), true,
skinAction(context.getSource(), GameProfileArgumentType.getProfileArgument(context, "targets"), true,
() -> MineskinSkinProvider.getSkin(StringArgumentType.getString(context, "url"), SkinVariant.SLIM))))))))
.then(literal("clear")
.executes(context ->
skinAction(Collections.singleton(context.getSource().getPlayer()), false,
() -> null))
.then(argument("targets", EntityArgumentType.players()).executes(context ->
skinAction(EntityArgumentType.getPlayers(context, "targets"), true,
() -> null))))
skinAction(context.getSource(), false,
() -> DEFAULT_SKIN))
.then(argument("targets", GameProfileArgumentType.gameProfile()).executes(context ->
skinAction(context.getSource(), GameProfileArgumentType.getProfileArgument(context, "targets"), true,
() -> DEFAULT_SKIN))))
);
}
private static int skinAction(Collection<ServerPlayerEntity> targets, boolean setByOperator, Supplier<Property> skinSupplier) {
new Thread(() -> {
if (!setByOperator)
targets.stream().findFirst().get().sendMessage(Text.of("§6[SkinRestorer]§f Downloading skin."), true);
Property skin = skinSupplier.get();
for (ServerPlayerEntity player : targets) {
SkinRestorer.getSkinStorage().setSkin(player.getUuid(), skin);
if (setByOperator)
player.sendMessage(Text.of("§a[SkinRestorer]§f Operator changed your skin. You need to reconnect to apply it."), true);
else
player.sendMessage(Text.of("§a[SkinRestorer]§f You need to reconnect to apply skin."), true);
private static int skinAction(ServerCommandSource src, Collection<GameProfile> targets, boolean setByOperator, Supplier<Property> skinSupplier) {
SkinRestorer.setSkinAsync(src.getServer(), targets, skinSupplier).thenAccept(pair -> {
Collection<GameProfile> profiles = pair.right();
Collection<ServerPlayerEntity> players = pair.left();
if (profiles.size() == 0) {
src.sendError(Text.of(TranslationUtils.translation.skinActionFailed));
return;
}
}).start();
if (setByOperator) {
src.sendFeedback(Text.of(
String.format(TranslationUtils.translation.skinActionAffectedProfile,
String.join(", ", profiles.stream().map(GameProfile::getName).toList()))), true);
if (players.size() != 0) {
src.sendFeedback(Text.of(
String.format(TranslationUtils.translation.skinActionAffectedPlayer,
String.join(", ", players.stream().map(p -> p.getGameProfile().getName()).toList()))), true);
}
} else {
src.sendFeedback(Text.of(TranslationUtils.translation.skinActionOk), true);
}
});
return targets.size();
}
private static int skinAction(ServerCommandSource src, boolean setByOperator, Supplier<Property> skinSupplier) {
if (src.getPlayer() != null) {
skinAction(src, Collections.singleton(src.getPlayer().getGameProfile()), setByOperator, skinSupplier);
return 1;
} else {
return 0;
}
}
}

View File

@@ -1,39 +1,29 @@
package net.lionarius.skinrestorer.mixin;
import com.mojang.authlib.properties.Property;
import net.lionarius.skinrestorer.MojangSkinProvider;
import net.lionarius.skinrestorer.SkinRestorer;
import net.lionarius.skinrestorer.SkinStorage;
import net.minecraft.network.ClientConnection;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.PlayerManager;
import net.minecraft.server.network.ServerPlayerEntity;
import org.spongepowered.asm.mixin.Final;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
@Mixin(PlayerManager.class)
public abstract class PlayerManagerMixin {
private static void applySkin(ServerPlayerEntity playerEntity, Property skin) {
playerEntity.getGameProfile().getProperties().removeAll("textures");
playerEntity.getGameProfile().getProperties().put("textures", skin);
}
@Shadow
public abstract List<ServerPlayerEntity> getPlayerList();
@Inject(method = "onPlayerConnect", at = @At(value = "HEAD"))
private void onPlayerConnect(ClientConnection connection, ServerPlayerEntity player, CallbackInfo ci) {
if (SkinRestorer.getSkinStorage().getSkin(player.getUuid()) == SkinStorage.DEFAULT_SKIN)
SkinRestorer.getSkinStorage().setSkin(player.getUuid(), MojangSkinProvider.getSkin(player.getGameProfile().getName()));
applySkin(player, SkinRestorer.getSkinStorage().getSkin(player.getUuid()));
}
@Shadow @Final private MinecraftServer server;
@Inject(method = "remove", at = @At("TAIL"))
private void remove(ServerPlayerEntity player, CallbackInfo ci) {
@@ -46,4 +36,11 @@ public abstract class PlayerManagerMixin {
SkinRestorer.getSkinStorage().removeSkin(player.getUuid());
}
}
@Inject(method = "onPlayerConnect", at = @At("HEAD"))
private void onPlayerConnected(ClientConnection connection, ServerPlayerEntity player, CallbackInfo ci) {
if (player.getClass() != ServerPlayerEntity.class) { // if the player isn't a server player entity, it must be someone's fake player
SkinRestorer.setSkinAsync(server, Collections.singleton(player.getGameProfile()), () -> SkinRestorer.getSkinStorage().getSkin(player.getUuid()));
}
}
}

View File

@@ -0,0 +1,68 @@
package net.lionarius.skinrestorer.mixin;
import com.mojang.authlib.GameProfile;
import com.mojang.authlib.properties.Property;
import net.lionarius.skinrestorer.MojangSkinProvider;
import net.lionarius.skinrestorer.SkinRestorer;
import net.lionarius.skinrestorer.SkinStorage;
import net.minecraft.network.packet.c2s.login.LoginHelloC2SPacket;
import net.minecraft.server.network.ServerLoginNetworkHandler;
import net.minecraft.server.network.ServerPlayerEntity;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.spongepowered.asm.mixin.Final;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import java.util.concurrent.CompletableFuture;
@Mixin(ServerLoginNetworkHandler.class)
public abstract class ServerLoginNetworkHandlerMixin {
@Shadow @Nullable GameProfile profile;
@Shadow protected abstract GameProfile toOfflineProfile(GameProfile profile);
@Shadow @Final
static Logger LOGGER;
private CompletableFuture<Property> skinrestorer_pendingSkin;
@Inject(method = "onHello", at = @At("RETURN"))
public void onHelloReturn(LoginHelloC2SPacket packet, CallbackInfo ci) {
assert profile != null;
GameProfile profile1;
if (!profile.isComplete()) {
profile1 = profile = toOfflineProfile(this.profile);
} else {
profile1 = profile;
}
skinrestorer_pendingSkin = CompletableFuture.supplyAsync(() -> {
LOGGER.debug("Fetching {}'s skin", profile1.getName());
if (SkinRestorer.getSkinStorage().getSkin(profile1.getId()) == SkinStorage.DEFAULT_SKIN)
SkinRestorer.getSkinStorage().setSkin(profile1.getId(), MojangSkinProvider.getSkin(profile1.getName()));
return SkinRestorer.getSkinStorage().getSkin(profile1.getId());
});
}
@Inject(method = "acceptPlayer", at = @At("HEAD"), cancellable = true)
public void waitForSkin(CallbackInfo ci) {
if (skinrestorer_pendingSkin != null && !skinrestorer_pendingSkin.isDone()) {
ci.cancel();
}
}
private static void applyRestoredSkin(ServerPlayerEntity playerEntity, Property skin) {
playerEntity.getGameProfile().getProperties().removeAll("textures");
playerEntity.getGameProfile().getProperties().put("textures", skin);
}
@Inject(method = "addToServer", at = @At("HEAD"))
public void applyRestoredSkinHook(ServerPlayerEntity player, CallbackInfo ci) {
if (skinrestorer_pendingSkin != null) {
applyRestoredSkin(player, skinrestorer_pendingSkin.getNow(SkinStorage.DEFAULT_SKIN));
}
}
}

View File

@@ -1,11 +1,12 @@
package net.lionarius.skinrestorer.util;
import java.io.*;
import java.nio.charset.StandardCharsets;
public class FileUtils {
public static String readFile(File file) {
try (BufferedReader reader = new BufferedReader(new FileReader(file))) {
try (BufferedReader reader = new BufferedReader(new FileReader(file, StandardCharsets.UTF_8))) {
return StringUtils.readString(reader);
} catch (IOException e) {
return null;
@@ -21,7 +22,7 @@ public class FileUtils {
if (!file.exists()) {
file.createNewFile();
}
try (FileWriter writer = new FileWriter(file)) {
try (FileWriter writer = new FileWriter(file, StandardCharsets.UTF_8)) {
writer.write(content);
}
return true;

View File

@@ -0,0 +1,27 @@
package net.lionarius.skinrestorer.util;
import net.lionarius.skinrestorer.SkinRestorer;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Objects;
public class TranslationUtils {
public static class Translation {
public String skinActionAffectedProfile = "Skin has been saved for %s";
public String skinActionAffectedPlayer = "Apply live skin changes for %s";
public String skinActionFailed = "Failed to set skin";
public String skinActionOk = "Skin changed";
}
public static Translation translation = new Translation();
static {
Path path = Path.of("config", "skinrestorer", "translation.json");
if (Files.exists(path)) {
try {
translation = JsonUtils.fromJson(Objects.requireNonNull(FileUtils.readFile(path.toFile())), Translation.class);
} catch (Exception ex) {
SkinRestorer.LOGGER.error("Failed to load translation", ex);
}
}
}
}

View File

@@ -24,7 +24,7 @@ public class WebUtils {
os.write(input.getBytes(StandardCharsets.UTF_8), 0, input.length());
}
try (BufferedReader br = new BufferedReader(new InputStreamReader(connection.getInputStream()))) {
try (BufferedReader br = new BufferedReader(new InputStreamReader(connection.getInputStream(), StandardCharsets.UTF_8))) {
return StringUtils.readString(br);
}
}
@@ -35,7 +35,7 @@ public class WebUtils {
connection.setRequestMethod("GET");
connection.setDoOutput(true);
try (BufferedReader br = new BufferedReader(new InputStreamReader(connection.getInputStream()))) {
try (BufferedReader br = new BufferedReader(new InputStreamReader(connection.getInputStream(), StandardCharsets.UTF_8))) {
return StringUtils.readString(br);
}
}

View File

@@ -5,7 +5,8 @@
"compatibilityLevel": "JAVA_16",
"mixins": [
"CommandManagerMixin",
"PlayerManagerMixin"
"PlayerManagerMixin",
"ServerLoginNetworkHandlerMixin"
],
"injectors": {
"defaultRequire": 1