mirror of
https://github.com/Suiranoil/SkinRestorer.git
synced 2026-01-16 04:42:12 +00:00
Let fake players have skin
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
package net.lionarius.skinrestorer.mixin;
|
||||
|
||||
import net.lionarius.skinrestorer.SkinRestorer;
|
||||
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;
|
||||
|
||||
@Mixin(PlayerManager.class)
|
||||
@@ -19,6 +23,8 @@ public abstract class PlayerManagerMixin {
|
||||
@Shadow
|
||||
public abstract List<ServerPlayerEntity> getPlayerList();
|
||||
|
||||
@Shadow @Final private MinecraftServer server;
|
||||
|
||||
@Inject(method = "remove", at = @At("TAIL"))
|
||||
private void remove(ServerPlayerEntity player, CallbackInfo ci) {
|
||||
SkinRestorer.getSkinStorage().removeSkin(player.getUuid());
|
||||
@@ -30,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()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,7 +54,7 @@ public abstract class ServerLoginNetworkHandlerMixin {
|
||||
}
|
||||
}
|
||||
|
||||
private static void applyRestoreSkin(ServerPlayerEntity playerEntity, Property skin) {
|
||||
private static void applyRestoredSkin(ServerPlayerEntity playerEntity, Property skin) {
|
||||
playerEntity.getGameProfile().getProperties().removeAll("textures");
|
||||
playerEntity.getGameProfile().getProperties().put("textures", skin);
|
||||
}
|
||||
@@ -62,7 +62,7 @@ public abstract class ServerLoginNetworkHandlerMixin {
|
||||
@Inject(method = "addToServer", at = @At("HEAD"))
|
||||
public void applyRestoredSkinHook(ServerPlayerEntity player, CallbackInfo ci) {
|
||||
if (skinrestorer_pendingSkin != null) {
|
||||
applyRestoreSkin(player, skinrestorer_pendingSkin.getNow(SkinStorage.DEFAULT_SKIN));
|
||||
applyRestoredSkin(player, skinrestorer_pendingSkin.getNow(SkinStorage.DEFAULT_SKIN));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user