From f1f5e204a251af50ee0bc139ae851d09e9faa48d Mon Sep 17 00:00:00 2001 From: 3944Realms Date: Fri, 20 Mar 2026 00:25:06 +0800 Subject: [PATCH] feat: nPC --- .../content/command/EDGCommand.java | 19 + .../content/entity/npc/INPCPlayer.java | 20 + .../content/entity/npc/NPCPlayerList.java | 437 ++++++++++++++++++ .../content/entity/npc/NPCRemotePlayer.java | 27 ++ .../content/entity/npc/NPCServerPlayer.java | 203 ++++++++ .../core/network/EDGNetworkHandler.java | 33 ++ .../network/NPCEmptyClientConnection.java | 35 ++ .../network/toClient/NPCInfoRemovePacket.java | 57 +++ .../network/toClient/NPCInfoUpdatePacket.java | 242 ++++++++++ .../core/storage/NPCDataStorage.java | 99 ++++ .../mixin/minecraft/MixinCamera.java | 2 +- .../mixin/minecraft/MixinClientConnect.java | 30 ++ .../minecraft/MixinClientPacketListener.java | 49 +- .../mixin/minecraft/MixinEntity.java | 19 +- .../mixin/minecraft/MixinEntityRenderer.java | 2 +- .../mixin/minecraft/MixinGameRender.java | 2 +- .../minecraft/MixinItemInHandRenderer.java | 2 +- .../mixin/minecraft/MixinLivingEntity.java | 4 +- .../mixin/minecraft/MixinMinecraft.java | 6 +- .../mixin/minecraft/MixinMinecraftServer.java | 88 ++++ .../mixin/minecraft/MixinPlayer.java | 37 +- .../mixin/minecraft/MixinPlayerRenderer.java | 4 +- .../mixin/minecraft/MixinServerPlayer.java | 2 +- .../util/IClientConnection.java | 23 + .../util/IEDGClientPacketListener.java | 49 ++ .../util/IEDGMinecraftServer.java | 24 + .../resources/eroticdungeongame.mixins.json | 3 + .../templates/META-INF/accesstransformer.cfg | 7 +- 28 files changed, 1503 insertions(+), 22 deletions(-) create mode 100644 src/main/java/top/r3944realms/eroticdungeongame/content/entity/npc/INPCPlayer.java create mode 100644 src/main/java/top/r3944realms/eroticdungeongame/content/entity/npc/NPCPlayerList.java create mode 100644 src/main/java/top/r3944realms/eroticdungeongame/content/entity/npc/NPCRemotePlayer.java create mode 100644 src/main/java/top/r3944realms/eroticdungeongame/content/entity/npc/NPCServerPlayer.java create mode 100644 src/main/java/top/r3944realms/eroticdungeongame/core/network/NPCEmptyClientConnection.java create mode 100644 src/main/java/top/r3944realms/eroticdungeongame/core/network/toClient/NPCInfoRemovePacket.java create mode 100644 src/main/java/top/r3944realms/eroticdungeongame/core/network/toClient/NPCInfoUpdatePacket.java create mode 100644 src/main/java/top/r3944realms/eroticdungeongame/core/storage/NPCDataStorage.java create mode 100644 src/main/java/top/r3944realms/eroticdungeongame/mixin/minecraft/MixinClientConnect.java create mode 100644 src/main/java/top/r3944realms/eroticdungeongame/mixin/minecraft/MixinMinecraftServer.java create mode 100644 src/main/java/top/r3944realms/eroticdungeongame/util/IClientConnection.java create mode 100644 src/main/java/top/r3944realms/eroticdungeongame/util/IEDGClientPacketListener.java create mode 100644 src/main/java/top/r3944realms/eroticdungeongame/util/IEDGMinecraftServer.java diff --git a/src/main/java/top/r3944realms/eroticdungeongame/content/command/EDGCommand.java b/src/main/java/top/r3944realms/eroticdungeongame/content/command/EDGCommand.java index ae3a3c6a..5b0fc939 100644 --- a/src/main/java/top/r3944realms/eroticdungeongame/content/command/EDGCommand.java +++ b/src/main/java/top/r3944realms/eroticdungeongame/content/command/EDGCommand.java @@ -36,6 +36,7 @@ import net.minecraft.network.chat.Style; import net.minecraft.server.level.ServerLevel; import net.minecraft.server.level.ServerPlayer; import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.GameType; import net.minecraft.world.level.block.entity.BlockEntity; import net.minecraft.world.level.block.state.BlockState; import net.minecraftforge.common.util.FakePlayerFactory; @@ -46,6 +47,7 @@ import top.r3944realms.eroticdungeongame.config.EDGConfig; import top.r3944realms.eroticdungeongame.content.block.ISeatBlock; import top.r3944realms.eroticdungeongame.content.block.blockentity.BaseSeatBlockEntity; import top.r3944realms.eroticdungeongame.content.entity.SeatEntity; +import top.r3944realms.eroticdungeongame.content.entity.npc.NPCServerPlayer; import top.r3944realms.eroticdungeongame.content.item.DeviceKeyItem; import top.r3944realms.eroticdungeongame.content.item.FilterItem; import top.r3944realms.eroticdungeongame.content.util.FurnitureHelper; @@ -121,6 +123,13 @@ public class EDGCommand extends SimpleHelpCommand { Commands.literal("norp") .executes(this::edg$norp) ) + .then( + Commands.literal("npc") + .then( + Commands.argument("name", StringArgumentType.word()) + .executes(this::edg$npc) + ) + ) .then( Commands.literal("device") .requires(source -> source.hasPermission(2)) @@ -496,6 +505,16 @@ public class EDGCommand extends SimpleHelpCommand { ); } + private int edg$npc(@NotNull CommandContext context) throws CommandSyntaxException { + //todo: 完善下内容 + CommandSourceStack source = context.getSource(); + ServerPlayer player = source.getPlayerOrException(); + String name = StringArgumentType.getString(context, "name"); + if (name.length() < 16) { + NPCServerPlayer.createNPC(name, source.getServer(), player.getX(), player.getY(), player.getZ(), source.getRotation().x, source.getRotation().y, source.getLevel().dimension(), GameType.SURVIVAL, false); + } + return 1; + } private int edg$norp(@NotNull CommandContext context) throws CommandSyntaxException { //todo: 完善下内容 CommandSourceStack source = context.getSource(); diff --git a/src/main/java/top/r3944realms/eroticdungeongame/content/entity/npc/INPCPlayer.java b/src/main/java/top/r3944realms/eroticdungeongame/content/entity/npc/INPCPlayer.java new file mode 100644 index 00000000..69c25a9b --- /dev/null +++ b/src/main/java/top/r3944realms/eroticdungeongame/content/entity/npc/INPCPlayer.java @@ -0,0 +1,20 @@ +/* + * Copyright 2025-2026 R3944Realms + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package top.r3944realms.eroticdungeongame.content.entity.npc; + +public interface INPCPlayer { +} diff --git a/src/main/java/top/r3944realms/eroticdungeongame/content/entity/npc/NPCPlayerList.java b/src/main/java/top/r3944realms/eroticdungeongame/content/entity/npc/NPCPlayerList.java new file mode 100644 index 00000000..91ad58c9 --- /dev/null +++ b/src/main/java/top/r3944realms/eroticdungeongame/content/entity/npc/NPCPlayerList.java @@ -0,0 +1,437 @@ +/* + * Copyright 2025-2026 R3944Realms + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package top.r3944realms.eroticdungeongame.content.entity.npc; + +import com.google.common.collect.Maps; +import com.google.common.collect.Sets; +import com.mojang.authlib.GameProfile; +import com.mojang.logging.LogUtils; +import com.mojang.serialization.Dynamic; +import io.netty.buffer.Unpooled; +import net.minecraft.ChatFormatting; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.core.LayeredRegistryAccess; +import net.minecraft.core.RegistryAccess; +import net.minecraft.core.RegistrySynchronization; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.NbtOps; +import net.minecraft.network.Connection; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.chat.*; +import net.minecraft.network.protocol.Packet; +import net.minecraft.network.protocol.game.*; +import net.minecraft.network.protocol.status.ServerStatus; +import net.minecraft.resources.ResourceKey; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.PlayerAdvancements; +import net.minecraft.server.RegistryLayer; +import net.minecraft.server.ServerScoreboard; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.server.network.ServerGamePacketListenerImpl; +import net.minecraft.server.players.GameProfileCache; +import net.minecraft.server.players.PlayerList; +import net.minecraft.stats.ServerStatsCounter; +import net.minecraft.stats.Stats; +import net.minecraft.tags.TagNetworkSerialization; +import net.minecraft.world.effect.MobEffectInstance; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.flag.FeatureFlags; +import net.minecraft.world.level.GameRules; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.biome.BiomeManager; +import net.minecraft.world.level.border.WorldBorder; +import net.minecraft.world.level.dimension.DimensionType; +import net.minecraft.world.level.storage.LevelData; +import net.minecraft.world.scores.Objective; +import net.minecraft.world.scores.PlayerTeam; +import org.jetbrains.annotations.NotNull; +import org.slf4j.Logger; +import top.r3944realms.eroticdungeongame.core.network.EDGNetworkHandler; +import top.r3944realms.eroticdungeongame.core.network.toClient.NPCInfoUpdatePacket; +import top.r3944realms.eroticdungeongame.core.storage.NPCDataStorage; + +import javax.annotation.Nullable; +import java.text.SimpleDateFormat; +import java.time.Instant; +import java.util.*; +import java.util.function.Function; +import java.util.function.Predicate; + +public class NPCPlayerList { + private static final Logger LOGGER = LogUtils.getLogger(); + private static final int SEND_PLAYER_INFO_INTERVAL = 600; + private static final SimpleDateFormat BAN_DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd 'at' HH:mm:ss z"); + private final LayeredRegistryAccess registries; + private final RegistryAccess.Frozen synchronizedRegistries; + private final NPCDataStorage npcIo; + private final MinecraftServer server; + private final List npcs = new ArrayList<>(); + private final Map npcsByUUID = Maps.newHashMap(); + private int sendAllNPCInfoIn; + private final List npcsView = java.util.Collections.unmodifiableList(npcs); + public static final Component CHAT_FILTERED_FULL = Component.translatable("chat.filtered_full"); + public NPCPlayerList(MinecraftServer server, LayeredRegistryAccess registries, NPCDataStorage npcDataStorage, int maxPlayers) { + this.server = server; + this.registries = registries; + this.synchronizedRegistries = (new RegistryAccess.ImmutableRegistryAccess(RegistrySynchronization.networkedRegistries(registries))).freeze(); + this.npcIo = npcDataStorage; + } + public void placeNewNPC(Connection netManager, NPCServerPlayer npc) { + GameProfile gameprofile = npc.getGameProfile(); + GameProfileCache gameprofilecache = this.server.getProfileCache(); + String s; + if (gameprofilecache != null) { + Optional optional = gameprofilecache.get(gameprofile.getId()); + s = optional.map(GameProfile::getName).orElse(gameprofile.getName()); + gameprofilecache.add(gameprofile); + } else { + s = gameprofile.getName(); + } + CompoundTag compoundtag = this.load(npc); + //noinspection deprecation + ResourceKey resourcekey = compoundtag != null ? DimensionType.parseLegacy(new Dynamic<>(NbtOps.INSTANCE, compoundtag.get("Dimension"))).resultOrPartial(LOGGER::error).orElse(Level.OVERWORLD) : Level.OVERWORLD; + ServerLevel serverlevel = this.server.getLevel(resourcekey); + ServerLevel serverlevel1; + if (serverlevel == null) { + LOGGER.warn("Unknown respawn dimension {}, defaulting to overworld", resourcekey); + serverlevel1 = this.server.overworld(); + } else { + serverlevel1 = serverlevel; + } + + npc.setServerLevel(serverlevel1); + String s1 = "local"; + //noinspection ConstantValue + if (netManager.getRemoteAddress() != null) { + s1 = net.minecraftforge.network.DualStackUtils.getAddressString(netManager.getRemoteAddress()); + } + LOGGER.info("[NPC] {}[{}] logged in with entity id {} at ({}, {}, {})", npc.getName().getString(), s1, npc.getId(), npc.getX(), npc.getY(), npc.getZ()); + LevelData leveldata = serverlevel1.getLevelData(); + npc.loadGameTypes(compoundtag); + ServerGamePacketListenerImpl servergamepacketlistenerimpl = new ServerGamePacketListenerImpl(this.server, netManager, npc); + net.minecraftforge.network.NetworkHooks.sendMCRegistryPackets(netManager, "PLAY_TO_CLIENT"); + GameRules gamerules = serverlevel1.getGameRules(); + boolean flag = gamerules.getBoolean(GameRules.RULE_DO_IMMEDIATE_RESPAWN); + boolean flag1 = gamerules.getBoolean(GameRules.RULE_REDUCEDDEBUGINFO); + servergamepacketlistenerimpl.send(new ClientboundLoginPacket(npc.getId(), leveldata.isHardcore(), npc.gameMode.getGameModeForPlayer(), npc.gameMode.getPreviousGameModeForPlayer(), this.server.levelKeys(), this.synchronizedRegistries, serverlevel1.dimensionTypeId(), serverlevel1.dimension(), BiomeManager.obfuscateSeed(serverlevel1.getSeed()), 0, 16, 16, flag1, !flag, serverlevel1.isDebug(), serverlevel1.isFlat(), npc.getLastDeathLocation(), npc.getPortalCooldown())); + servergamepacketlistenerimpl.send(new ClientboundUpdateEnabledFeaturesPacket(FeatureFlags.REGISTRY.toNames(serverlevel1.enabledFeatures()))); + servergamepacketlistenerimpl.send(new ClientboundCustomPayloadPacket(ClientboundCustomPayloadPacket.BRAND, (new FriendlyByteBuf(Unpooled.buffer())).writeUtf(this.getServer().getServerModName()))); + servergamepacketlistenerimpl.send(new ClientboundChangeDifficultyPacket(leveldata.getDifficulty(), leveldata.isDifficultyLocked())); + servergamepacketlistenerimpl.send(new ClientboundPlayerAbilitiesPacket(npc.getAbilities())); + servergamepacketlistenerimpl.send(new ClientboundSetCarriedItemPacket(npc.getInventory().selected)); + // todo: 数据同步事件(对于NPC类) 可以搞个继承类复用这个事件 +// net.minecraftforge.common.MinecraftForge.EVENT_BUS.post(new net.minecraftforge.event.OnDatapackSyncEvent(this, npc)); + servergamepacketlistenerimpl.send(new ClientboundUpdateRecipesPacket(this.server.getRecipeManager().getRecipes())); + servergamepacketlistenerimpl.send(new ClientboundUpdateTagsPacket(TagNetworkSerialization.serializeTagsToNetwork(this.registries))); + this.sendPlayerPermissionLevel(npc); + npc.getStats().markAllDirty(); + npc.getRecipeBook().sendInitialRecipeBook(npc); + this.updateEntireScoreboard(serverlevel1.getScoreboard(), npc); + this.server.invalidateStatus(); + MutableComponent mutablecomponent; + if (npc.getGameProfile().getName().equalsIgnoreCase(s)) { //todo npc + mutablecomponent = Component.translatable("multiplayer.player.joined", npc.getDisplayName()); + } else { + mutablecomponent = Component.translatable("multiplayer.player.joined.renamed", npc.getDisplayName(), s); + } + + this.broadcastSystemMessage(mutablecomponent.withStyle(ChatFormatting.YELLOW), false); + servergamepacketlistenerimpl.teleport(npc.getX(), npc.getY(), npc.getZ(), npc.getYRot(), npc.getXRot()); + ServerStatus serverstatus = this.server.getStatus(); + if (serverstatus != null) { + npc.sendServerStatus(serverstatus); + } + //todo 实现网络传递 + EDGNetworkHandler.sendToPlayer(NPCInfoUpdatePacket.createNPCPlayerInitializing(this.npcs), npc); + this.npcs.add(npc); + this.npcsByUUID.put(npc.getUUID(), npc); + EDGNetworkHandler.sendToAllPlayer(NPCInfoUpdatePacket.createNPCPlayerInitializing(List.of(npc))); + this.sendLevelInfo(npc, serverlevel1); + serverlevel1.addNewPlayer(npc); + this.server.getCustomBossEvents().onPlayerConnect(npc); + this.server.getServerResourcePack().ifPresent((serverResourcePackInfo) -> npc.sendTexturePack(serverResourcePackInfo.url(), serverResourcePackInfo.hash(), serverResourcePackInfo.isRequired(), serverResourcePackInfo.prompt())); + + for(MobEffectInstance mobeffectinstance : npc.getActiveEffects()) { + servergamepacketlistenerimpl.send(new ClientboundUpdateMobEffectPacket(npc.getId(), mobeffectinstance)); + } + + if (compoundtag != null && compoundtag.contains("RootVehicle", 10)) { + CompoundTag compoundtag1 = compoundtag.getCompound("RootVehicle"); + Entity entity1 = EntityType.loadEntityRecursive(compoundtag1.getCompound("Entity"), serverlevel1, (p_215603_) -> !serverlevel1.addWithUUID(p_215603_) ? null : p_215603_); + if (entity1 != null) { + UUID uuid; + if (compoundtag1.hasUUID("Attach")) { + uuid = compoundtag1.getUUID("Attach"); + } else { + uuid = null; + } + + if (entity1.getUUID().equals(uuid)) { + npc.startRiding(entity1, true); + } else { + for(Entity entity : entity1.getIndirectPassengers()) { + if (entity.getUUID().equals(uuid)) { + npc.startRiding(entity, true); + break; + } + } + } + + if (!npc.isPassenger()) { + LOGGER.warn("Couldn't reattach entity to player"); + entity1.discard(); + + for(Entity entity2 : entity1.getIndirectPassengers()) { + entity2.discard(); + } + } + } + } + + npc.initInventoryMenu(); + net.minecraftforge.event.ForgeEventFactory.firePlayerLoggedIn(npc); + } + public void sendPlayerPermissionLevel(@NotNull ServerPlayer player) { + GameProfile gameprofile = player.getGameProfile(); + int i = this.server.getProfilePermissions(gameprofile); + this.sendPlayerPermissionLevel(player, i); + } + + private void sendPlayerPermissionLevel(@NotNull ServerPlayer player, int permLevel) { + byte b0; + if (permLevel <= 0) { + b0 = 24; + } else if (permLevel >= 4) { + b0 = 28; + } else { + b0 = (byte)(24 + permLevel); + } + + player.connection.send(new ClientboundEntityEventPacket(player, b0)); + + this.server.getCommands().sendCommands(player); + } + /** + * Updates the time and weather for the given player to those of the given world + */ + public void sendLevelInfo(@NotNull ServerPlayer player, @NotNull ServerLevel level) { + WorldBorder worldborder = this.server.overworld().getWorldBorder(); + player.connection.send(new ClientboundInitializeBorderPacket(worldborder)); + player.connection.send(new ClientboundSetTimePacket(level.getGameTime(), level.getDayTime(), level.getGameRules().getBoolean(GameRules.RULE_DAYLIGHT))); + player.connection.send(new ClientboundSetDefaultSpawnPositionPacket(level.getSharedSpawnPos(), level.getSharedSpawnAngle())); + if (level.isRaining()) { + player.connection.send(new ClientboundGameEventPacket(ClientboundGameEventPacket.START_RAINING, 0.0F)); + player.connection.send(new ClientboundGameEventPacket(ClientboundGameEventPacket.RAIN_LEVEL_CHANGE, level.getRainLevel(1.0F))); + player.connection.send(new ClientboundGameEventPacket(ClientboundGameEventPacket.THUNDER_LEVEL_CHANGE, level.getThunderLevel(1.0F))); + } + + } + protected void updateEntireScoreboard(@NotNull ServerScoreboard scoreboard, ServerPlayer player) { + Set set = Sets.newHashSet(); + + for(PlayerTeam playerteam : scoreboard.getPlayerTeams()) { + player.connection.send(ClientboundSetPlayerTeamPacket.createAddOrModifyPacket(playerteam, true)); + } + + for(int i = 0; i < 19; ++i) { + Objective objective = scoreboard.getDisplayObjective(i); + if (objective != null && !set.contains(objective)) { + for(Packet packet : scoreboard.getStartTrackingPackets(objective)) { + player.connection.send(packet); + } + + set.add(objective); + } + } + + } + /** + * Called during player login. Reads the player information from disk. + */ + @Nullable + public CompoundTag load(@NotNull NPCServerPlayer player) { + CompoundTag compoundtag = this.server.getWorldData().getLoadedPlayerTag(); + CompoundTag compoundtag1; + if (this.server.isSingleplayerOwner(player.getGameProfile()) && compoundtag != null) { + compoundtag1 = compoundtag; + player.load(compoundtag); + LOGGER.debug("loading single npc"); +// net.minecraftforge.event.ForgeEventFactory.firePlayerLoadingEvent(player, this.npcIo, player.getUUID().toString()); + } else { + compoundtag1 = this.npcIo.load(player); + } + + return compoundtag1; + } + public void broadcastSystemMessage(Component message, boolean bypassHiddenChat) { + this.broadcastSystemMessage(message, (p_215639_) -> message, bypassHiddenChat); + } + public void saveAll() { + for (NPCServerPlayer npc : this.npcs) { + this.save(npc); + } + } + + protected void save(NPCServerPlayer npcServerPlayer) { + this.npcIo.save(npcServerPlayer); + } + public void removeAll() { + for (NPCServerPlayer npc : this.npcs) { + npc.connection.disconnect(Component.translatable("multiplayer.disconnect.server_shutdown")); + } + + } + + public void remove(NPCServerPlayer npc) { + net.minecraftforge.event.ForgeEventFactory.firePlayerLoggedOut(npc); + ServerLevel serverlevel = npc.serverLevel(); + npc.awardStat(Stats.LEAVE_GAME); + this.save(npc); + if (npc.isPassenger()) { + Entity entity = npc.getRootVehicle(); + if (entity.hasExactlyOnePlayerPassenger()) { + LOGGER.debug("Removing player mount"); + npc.stopRiding(); + entity.getPassengersAndSelf().forEach((p_215620_) -> { + p_215620_.setRemoved(Entity.RemovalReason.UNLOADED_WITH_PLAYER); + }); + } + } + + npc.unRide(); + serverlevel.removePlayerImmediately(npc, Entity.RemovalReason.UNLOADED_WITH_PLAYER); + npc.getAdvancements().stopListening(); + this.npcs.remove(npc); + this.server.getCustomBossEvents().onPlayerDisconnect(npc); + UUID uuid = npc.getUUID(); + ServerPlayer serverplayer = this.npcsByUUID.get(uuid); + if (serverplayer == npc) { + this.npcsByUUID.remove(uuid); + } + + this.broadcastAll(new ClientboundPlayerInfoRemovePacket(List.of(npc.getUUID()))); + } + + public void broadcastSystemMessage(Component serverMessage, Function playerMessageFactory, boolean bypassHiddenChat) { + this.server.sendSystemMessage(serverMessage); + PlayerList playerList = server.getPlayerList(); + for(ServerPlayer serverplayer : playerList.getPlayers()) { + Component component = playerMessageFactory.apply(serverplayer); + if (component != null) { + serverplayer.sendSystemMessage(component, bypassHiddenChat); + } + } + for(ServerPlayer serverplayer : this.npcs) { + Component component = playerMessageFactory.apply(serverplayer); + if (component != null) { + serverplayer.sendSystemMessage(component, bypassHiddenChat); + } + } + + } + + public void tick() { + if (++this.sendAllNPCInfoIn > 600) { + EDGNetworkHandler.sendToAllPlayer(new NPCInfoUpdatePacket(EnumSet.of(NPCInfoUpdatePacket.Action.UPDATE_LATENCY), this.npcs)); + this.sendAllNPCInfoIn = 0; + } + + } + + public void broadcastChatMessage(PlayerChatMessage message, @NotNull CommandSourceStack sender, ChatType.Bound boundChatType) { + this.broadcastChatMessage(message, sender::shouldFilterMessageTo, sender.getPlayer(), boundChatType); + } + + public void broadcastChatMessage(PlayerChatMessage message, ServerPlayer sender, ChatType.Bound boundChatType) { + this.broadcastChatMessage(message, sender::shouldFilterMessageTo, sender, boundChatType); + } + + private void broadcastChatMessage(PlayerChatMessage message, Predicate shouldFilterMessageTo, @Nullable ServerPlayer sender, ChatType.Bound boundChatType) { + boolean flag = this.verifyChatTrusted(message); + this.server.logChatMessage(message.decoratedContent(), boundChatType, flag ? null : "Not Secure"); + OutgoingChatMessage outgoingchatmessage = OutgoingChatMessage.create(message); + boolean flag1 = false; + PlayerList playerList = server.getPlayerList(); + for(ServerPlayer serverplayer : playerList.getPlayers()) { + boolean flag2 = shouldFilterMessageTo.test(serverplayer); + serverplayer.sendChatMessage(outgoingchatmessage, flag2, boundChatType); + flag1 |= flag2 && message.isFullyFiltered(); + } + for(ServerPlayer serverplayer : this.npcs) { + boolean flag2 = shouldFilterMessageTo.test(serverplayer); + serverplayer.sendChatMessage(outgoingchatmessage, flag2, boundChatType); + flag1 |= flag2 && message.isFullyFiltered(); + } + + if (flag1 && sender != null) { + sender.sendSystemMessage(CHAT_FILTERED_FULL); + } + + } + public void broadcastAll(Packet packet) { + PlayerList playerList = server.getPlayerList(); + for(ServerPlayer serverplayer : playerList.getPlayers()) { + serverplayer.connection.send(packet); + } + for(ServerPlayer serverplayer : this.npcs) { + serverplayer.connection.send(packet); + } + + } + + public void broadcastAll(Packet packet, ResourceKey dimension) { + PlayerList playerList = server.getPlayerList(); + for(ServerPlayer serverplayer : playerList.getPlayers()) { + if (serverplayer.level().dimension() == dimension) { + serverplayer.connection.send(packet); + } + } + for(ServerPlayer serverplayer : this.npcs) { + if (serverplayer.level().dimension() == dimension) { + serverplayer.connection.send(packet); + } + } + + } + + private boolean verifyChatTrusted(@NotNull PlayerChatMessage message) { + return message.hasSignature() && !message.hasExpiredServer(Instant.now()); + } + + public List getPlayers() { + return this.npcsView; //Unmodifiable view + } + + @Nullable + public ServerPlayer getNPC(UUID npcUUID) { + return this.npcsByUUID.get(npcUUID); + } + public void reloadResources() { + this.broadcastAll(new ClientboundUpdateTagsPacket(TagNetworkSerialization.serializeTagsToNetwork(this.registries))); + ClientboundUpdateRecipesPacket clientboundupdaterecipespacket = new ClientboundUpdateRecipesPacket(this.server.getRecipeManager().getRecipes()); + for(ServerPlayer serverplayer : this.npcs) { + serverplayer.connection.send(clientboundupdaterecipespacket); + serverplayer.getRecipeBook().sendInitialRecipeBook(serverplayer); + } + + } + public MinecraftServer getServer() { + return this.server; + } +} diff --git a/src/main/java/top/r3944realms/eroticdungeongame/content/entity/npc/NPCRemotePlayer.java b/src/main/java/top/r3944realms/eroticdungeongame/content/entity/npc/NPCRemotePlayer.java new file mode 100644 index 00000000..95e81e0d --- /dev/null +++ b/src/main/java/top/r3944realms/eroticdungeongame/content/entity/npc/NPCRemotePlayer.java @@ -0,0 +1,27 @@ +/* + * Copyright 2025-2026 R3944Realms + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package top.r3944realms.eroticdungeongame.content.entity.npc; + +import com.mojang.authlib.GameProfile; +import net.minecraft.client.multiplayer.ClientLevel; +import net.minecraft.client.player.RemotePlayer; + +public class NPCRemotePlayer extends RemotePlayer implements INPCPlayer { + public NPCRemotePlayer(ClientLevel clientLevel, GameProfile gameProfile) { + super(clientLevel, gameProfile); + } +} diff --git a/src/main/java/top/r3944realms/eroticdungeongame/content/entity/npc/NPCServerPlayer.java b/src/main/java/top/r3944realms/eroticdungeongame/content/entity/npc/NPCServerPlayer.java new file mode 100644 index 00000000..2909bfdf --- /dev/null +++ b/src/main/java/top/r3944realms/eroticdungeongame/content/entity/npc/NPCServerPlayer.java @@ -0,0 +1,203 @@ +/* + * Copyright 2025-2026 R3944Realms + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package top.r3944realms.eroticdungeongame.content.entity.npc; + +import com.mojang.authlib.GameProfile; +import dev.dubhe.curtain.utils.Messenger; +import net.minecraft.core.BlockPos; +import net.minecraft.core.UUIDUtil; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.chat.Component; +import net.minecraft.network.protocol.PacketFlow; +import net.minecraft.network.protocol.game.ClientboundRotateHeadPacket; +import net.minecraft.network.protocol.game.ClientboundTeleportEntityPacket; +import net.minecraft.network.protocol.game.ServerboundClientCommandPacket; +import net.minecraft.resources.ResourceKey; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.TickTask; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.server.players.GameProfileCache; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EquipmentSlot; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.food.FoodData; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.GameType; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.entity.SkullBlockEntity; +import net.minecraft.world.level.block.state.BlockState; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import top.r3944realms.eroticdungeongame.EroticDungeon; +import top.r3944realms.eroticdungeongame.core.network.NPCEmptyClientConnection; + +import java.util.Iterator; +import java.util.Objects; +import java.util.concurrent.atomic.AtomicReference; + +public class NPCServerPlayer extends ServerPlayer implements INPCPlayer { + public Runnable fixStartingPosition = () -> { + }; + public NPCServerPlayer(MinecraftServer server, ServerLevel level, GameProfile gameProfile) { + super(server, level, gameProfile); + } + + public static @Nullable NPCServerPlayer createNPC(String username, @NotNull MinecraftServer server, double x, double y, double z, double yaw, double pitch, ResourceKey dimensionId, GameType gamemode, boolean isflying) { + ServerLevel worldIn = server.getLevel(dimensionId); + if (worldIn != null) { + GameProfileCache.setUsesAuthentication(false); + GameProfile gameProfile = null; + try { + GameProfileCache profileCache = server.getProfileCache(); + if (profileCache != null) { + gameProfile = profileCache.get(username).orElse(null); + } + } finally { + GameProfileCache.setUsesAuthentication(server.isDedicatedServer() && server.usesAuthentication()); + } + try { + if (gameProfile == null) { + gameProfile = new GameProfile(UUIDUtil.createOfflinePlayerUUID(username), username); + } + + if (gameProfile.getProperties().containsKey("textures")) { + AtomicReference result = new AtomicReference<>(); + Objects.requireNonNull(result); + SkullBlockEntity.updateGameprofile(gameProfile, result::set); + gameProfile = result.get(); + } + NPCServerPlayer instance = new NPCServerPlayer(server, worldIn, gameProfile); + instance.fixStartingPosition = () -> { + instance.moveTo(x, y, z, (float) yaw, (float) pitch); + }; + server.getPlayerList().placeNewPlayer(new NPCEmptyClientConnection(PacketFlow.SERVERBOUND), instance); + instance.teleportTo(worldIn, x, y, z, (float) yaw, (float) pitch); + instance.setHealth(20.0F); + instance.unsetRemoved(); + instance.setMaxUpStep(0.6F); + instance.gameMode.changeGameModeForPlayer(gamemode); + server.getPlayerList().broadcastAll(new ClientboundRotateHeadPacket(instance, (byte)((int)(instance.yHeadRot * 256.0F / 360.0F))), dimensionId); + server.getPlayerList().broadcastAll(new ClientboundTeleportEntityPacket(instance), dimensionId); + instance.entityData.set(DATA_PLAYER_MODE_CUSTOMISATION, (byte)127); + instance.getAbilities().flying = isflying; + return instance; + } catch (Exception e) { + EroticDungeon.getLogger().error("Failed to create NPC", e); + } + } else EroticDungeon.getLogger().error("Failed to create NPC because server({}) is null!", dimensionId); + return null; + } + + + public void onEquipItem(EquipmentSlot p_238393_, ItemStack p_238394_, ItemStack p_238395_) { + if (!this.isUsingItem()) { + super.onEquipItem(p_238393_, p_238394_, p_238395_); + } + + } + + public void kill() { + this.kill(Messenger.s("Killed")); + } + + public void kill(Component reason) { + this.shakeOff(); + this.server.tell(new TickTask(this.server.getTickCount(), () -> { + this.connection.onDisconnect(reason); + })); + } + + private void shakeOff() { + if (this.getVehicle() instanceof Player) { + this.stopRiding(); + } + + Iterator var1 = this.getIndirectPassengers().iterator(); + + while(var1.hasNext()) { + Entity passenger = (Entity)var1.next(); + if (passenger instanceof Player) { + passenger.stopRiding(); + } + } + + } + + public void die(@NotNull DamageSource damageSource) { + this.shakeOff(); + super.die(damageSource); + this.setHealth(20.0F); + this.foodData = new FoodData(); + this.kill(this.getCombatTracker().getDeathMessage()); + } + + @Override + public void addAdditionalSaveData(@NotNull CompoundTag compound) { + super.addAdditionalSaveData(compound); + } + + @Override + public void readAdditionalSaveData(@NotNull CompoundTag compound) { + super.readAdditionalSaveData(compound); + } + + @Override + public @NotNull String getIpAddress() { + return "127.0.0.1"; + } + + @Override + public @NotNull Component getName() { + return Component.literal("[NPC]").append(super.getName()); + } + + @Override + public void tick() { + if (this.getServer() != null && this.getServer().getTickCount() % 10 == 0) { + this.connection.resetPosition(); + ((ServerLevel)this.level()).getChunkSource().move(this); + } + + try { + super.tick(); + this.doTick(); + } catch (NullPointerException ignored) { + } + + } + + public Entity changeDimension(@NotNull ServerLevel level) { + super.changeDimension(level); + if (this.wonGame) { + ServerboundClientCommandPacket packet = new ServerboundClientCommandPacket(net.minecraft.network.protocol.game.ServerboundClientCommandPacket.Action.PERFORM_RESPAWN); + this.connection.handleClientCommand(packet); + } + + if (this.connection.player.isChangingDimension()) { + this.connection.player.hasChangedDimension(); + } + + return this.connection.player; + } + + protected void checkFallDamage(double y, boolean onGround, @NotNull BlockState state, @NotNull BlockPos pos) { + this.doCheckFallDamage(this.getX(), y, this.getZ(), onGround); + } + +} diff --git a/src/main/java/top/r3944realms/eroticdungeongame/core/network/EDGNetworkHandler.java b/src/main/java/top/r3944realms/eroticdungeongame/core/network/EDGNetworkHandler.java index 66b81440..e0caf51f 100644 --- a/src/main/java/top/r3944realms/eroticdungeongame/core/network/EDGNetworkHandler.java +++ b/src/main/java/top/r3944realms/eroticdungeongame/core/network/EDGNetworkHandler.java @@ -16,16 +16,31 @@ package top.r3944realms.eroticdungeongame.core.network; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerPlayer; import net.minecraftforge.network.NetworkDirection; import net.minecraftforge.network.NetworkRegistry; +import net.minecraftforge.network.PacketDistributor; import net.minecraftforge.network.simple.SimpleChannel; +import net.minecraftforge.server.ServerLifecycleHooks; +import org.jetbrains.annotations.NotNull; import top.r3944realms.eroticdungeongame.EroticDungeon; +import top.r3944realms.eroticdungeongame.core.network.toClient.NPCInfoRemovePacket; +import top.r3944realms.eroticdungeongame.core.network.toClient.NPCInfoUpdatePacket; import top.r3944realms.eroticdungeongame.core.network.toServer.LoveMachineConfigPacket; import top.r3944realms.eroticdungeongame.core.network.toServer.RequestQuitDevicePacket; +import top.r3944realms.eroticdungeongame.util.IEDGMinecraftServer; import java.util.concurrent.atomic.AtomicInteger; public class EDGNetworkHandler { + public static final PacketDistributor ALL_PLAYER = new PacketDistributor<>((voidPacketDistributor, supplier) -> { + MinecraftServer minecraftServer = ServerLifecycleHooks.getCurrentServer(); + if (minecraftServer instanceof IEDGMinecraftServer iedgMinecraftServer) { + return packet -> iedgMinecraftServer.getNPCPlayerList().broadcastAll(packet); + } + return p -> {}; + }, NetworkDirection.PLAY_TO_CLIENT); public static final SimpleChannel CHANNEL = NetworkRegistry.newSimpleChannel( EroticDungeon.rl("main"), () -> EroticDungeon.ModInfo.VERSION, @@ -44,8 +59,26 @@ public class EDGNetworkHandler { .decoder(RequestQuitDevicePacket::new) .consumerMainThread(RequestQuitDevicePacket::handle) .add(); + CHANNEL.messageBuilder(NPCInfoRemovePacket.class, getIndex(), NetworkDirection.PLAY_TO_CLIENT) + .encoder(NPCInfoRemovePacket::write) + .decoder(NPCInfoRemovePacket::new) + .consumerMainThread(NPCInfoRemovePacket::handle) + .add(); + CHANNEL.messageBuilder(NPCInfoUpdatePacket.class, getIndex(), NetworkDirection.PLAY_TO_CLIENT) + .encoder(NPCInfoUpdatePacket::write) + .decoder(NPCInfoUpdatePacket::new) + .consumerMainThread(NPCInfoUpdatePacket::handle) + .add(); } public static int getIndex() { return cid.getAndIncrement(); } + + public static void sendToPlayer(MSG message, ServerPlayer player){ + CHANNEL.send(PacketDistributor.PLAYER.with(() -> player), message); + } + + public static void sendToAllPlayer(MSG message){ + CHANNEL.send(ALL_PLAYER.noArg(), message); + } } diff --git a/src/main/java/top/r3944realms/eroticdungeongame/core/network/NPCEmptyClientConnection.java b/src/main/java/top/r3944realms/eroticdungeongame/core/network/NPCEmptyClientConnection.java new file mode 100644 index 00000000..d7df4162 --- /dev/null +++ b/src/main/java/top/r3944realms/eroticdungeongame/core/network/NPCEmptyClientConnection.java @@ -0,0 +1,35 @@ +/* + * Copyright 2025-2026 R3944Realms + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package top.r3944realms.eroticdungeongame.core.network; + +import io.netty.channel.embedded.EmbeddedChannel; +import net.minecraft.network.Connection; +import net.minecraft.network.protocol.PacketFlow; +import top.r3944realms.eroticdungeongame.util.IClientConnection; + +public class NPCEmptyClientConnection extends Connection { + public NPCEmptyClientConnection(PacketFlow packetFlow) { + super(packetFlow); + ((IClientConnection)this).setChannel(new EmbeddedChannel()); + } + + public void setReadOnly() { + } + + public void handleDisconnection() { + } +} diff --git a/src/main/java/top/r3944realms/eroticdungeongame/core/network/toClient/NPCInfoRemovePacket.java b/src/main/java/top/r3944realms/eroticdungeongame/core/network/toClient/NPCInfoRemovePacket.java new file mode 100644 index 00000000..c4991820 --- /dev/null +++ b/src/main/java/top/r3944realms/eroticdungeongame/core/network/toClient/NPCInfoRemovePacket.java @@ -0,0 +1,57 @@ +/* + * Copyright 2025-2026 R3944Realms + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package top.r3944realms.eroticdungeongame.core.network.toClient; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.multiplayer.ClientPacketListener; +import net.minecraft.client.multiplayer.PlayerInfo; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraftforge.network.NetworkEvent; +import org.jetbrains.annotations.NotNull; +import top.r3944realms.eroticdungeongame.util.IEDGClientPacketListener; + +import java.util.List; +import java.util.UUID; +import java.util.function.Supplier; + +public record NPCInfoRemovePacket(List profileIds) { + public NPCInfoRemovePacket(@NotNull FriendlyByteBuf buf) { + this(buf.readList(FriendlyByteBuf::readUUID)); + } + + public void write(@NotNull FriendlyByteBuf buffer) { + buffer.writeCollection(this.profileIds, FriendlyByteBuf::writeUUID); + } + + + public static void handle(NPCInfoRemovePacket packet, @NotNull Supplier contextSupplier) { + NetworkEvent.Context context = contextSupplier.get(); + context.enqueueWork(() -> { + for(UUID uuid : packet.profileIds()) { + Minecraft minecraft = Minecraft.getInstance(); + ClientPacketListener clientPacketListener = minecraft.getConnection(); + if(clientPacketListener instanceof IEDGClientPacketListener iedgClientPacketListener) { + PlayerInfo playerinfo = iedgClientPacketListener.getNPCPlayerInfoMap().remove(uuid); + if (playerinfo != null) { + iedgClientPacketListener.getListedNPCPlayers().remove(playerinfo); + } + } + } + }); + context.setPacketHandled(true); + } +} diff --git a/src/main/java/top/r3944realms/eroticdungeongame/core/network/toClient/NPCInfoUpdatePacket.java b/src/main/java/top/r3944realms/eroticdungeongame/core/network/toClient/NPCInfoUpdatePacket.java new file mode 100644 index 00000000..b99c4a96 --- /dev/null +++ b/src/main/java/top/r3944realms/eroticdungeongame/core/network/toClient/NPCInfoUpdatePacket.java @@ -0,0 +1,242 @@ +/* + * Copyright 2025-2026 R3944Realms + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package top.r3944realms.eroticdungeongame.core.network.toClient; + +import com.eliotlash.mclib.math.functions.limit.Min; +import com.mojang.authlib.GameProfile; +import net.minecraft.Optionull; +import net.minecraft.client.Minecraft; +import net.minecraft.client.multiplayer.ClientPacketListener; +import net.minecraft.client.multiplayer.PlayerInfo; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.RemoteChatSession; +import net.minecraft.network.protocol.game.ClientboundPlayerInfoUpdatePacket; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.level.GameType; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.api.distmarker.OnlyIn; +import net.minecraftforge.network.NetworkEvent; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import top.r3944realms.eroticdungeongame.EroticDungeon; +import top.r3944realms.eroticdungeongame.content.entity.npc.NPCServerPlayer; +import top.r3944realms.eroticdungeongame.util.IEDGClientPacketListener; + +import java.util.*; +import java.util.function.Supplier; + +public final class NPCInfoUpdatePacket { + private final EnumSet actions; + private final List entries; + + public List newEntries() { + return this.actions.contains(Action.ADD_NPC) ? this.entries : List.of(); + } + + public NPCInfoUpdatePacket(EnumSet actions, List npcs) { + this.actions = actions; + this.entries = npcs; + } + + public NPCInfoUpdatePacket(Action action, NPCServerPlayer npc) { + this(EnumSet.of(action), List.of(new Entry(npc))); + } + + public NPCInfoUpdatePacket(EnumSet actions, @NotNull Collection players) { + this(actions, players.stream().map(Entry::new).toList()); + } + + public NPCInfoUpdatePacket(@NotNull FriendlyByteBuf buf) { + this.actions = EnumSet.noneOf(Action.class); + this.entries = buf.readList((read) -> { + EntryBuilder builder = new EntryBuilder(read.readUUID()); + + for(Action action : this.actions) { + action.reader.read(builder, read); + } + + return builder.build(); + }); + } + public void write(@NotNull FriendlyByteBuf buffer) { + buffer.writeEnumSet(this.actions, Action.class); + buffer.writeCollection(this.entries, (buf, entry) -> { + buf.writeUUID(entry.profileId()); + + for(Action action : this.actions) { + action.writer.write(buf, entry); + } + }); + } + public static @NotNull NPCInfoUpdatePacket createNPCPlayerInitializing(Collection players) { + EnumSet enumset = EnumSet.of(Action.ADD_NPC, Action.INITIALIZE_CHAT, Action.UPDATE_GAME_MODE, Action.UPDATE_LISTED, Action.UPDATE_LATENCY, Action.UPDATE_DISPLAY_NAME); + return new NPCInfoUpdatePacket(enumset, players); + } + + public static void handle(NPCInfoUpdatePacket packet, @NotNull Supplier contextSupplier) { + NetworkEvent.Context context = contextSupplier.get(); + context.enqueueWork(() -> { + Minecraft minecraft = Minecraft.getInstance(); + ClientPacketListener clientPacketListener = minecraft.getConnection(); + if (clientPacketListener instanceof IEDGClientPacketListener iedgClientPacketListener) { + for (Entry entry : packet.newEntries()) { + PlayerInfo npcInfo = new PlayerInfo(entry.profile, false); + iedgClientPacketListener.getNPCPlayerInfoMap().putIfAbsent(entry.profileId(), npcInfo); + } + for (Entry entry : packet.entries) { + PlayerInfo npcInfo = iedgClientPacketListener.getNPCPlayerInfoMap().get(entry.profileId); + if (npcInfo == null) { + EroticDungeon.getLogger().warn("Ignoring npc player info update for unknown npc player {}", entry.profileId()); + } else { + for(Action action : packet.actions()) { + applyPlayerInfoUpdate(action, entry, npcInfo); + } + } + } + } + }); + context.setPacketHandled(true); + } + @OnlyIn(Dist.CLIENT) + private static void applyPlayerInfoUpdate(@NotNull Action action, Entry entry, PlayerInfo playerInfo) { + Minecraft minecraft = Minecraft.getInstance(); + ClientPacketListener clientPacketListener = minecraft.getConnection(); + if (clientPacketListener instanceof IEDGClientPacketListener iedgClientPacketListener) { + switch (action) { + case INITIALIZE_CHAT: + ClientboundPlayerInfoUpdatePacket.Entry copyEntry = new ClientboundPlayerInfoUpdatePacket.Entry( + entry.profileId, entry.profile, entry.listed, entry.latency, entry.gameMode, entry.displayName, entry.chatSession + ); + clientPacketListener.initializeChatSession(copyEntry, playerInfo); + break; + case UPDATE_GAME_MODE: + + if (playerInfo.getGameMode() != entry.gameMode() && minecraft.player != null && minecraft.player.getUUID().equals(entry.profileId())) { + minecraft.player.onGameModeChanged(entry.gameMode()); + } + + playerInfo.setGameMode(entry.gameMode()); + break; + case UPDATE_LISTED: + if (entry.listed()) { + iedgClientPacketListener.getListedNPCPlayers().add(playerInfo); + } else { + iedgClientPacketListener.getListedNPCPlayers().remove(playerInfo); + } + break; + case UPDATE_LATENCY: + playerInfo.setLatency(entry.latency()); + break; + case UPDATE_DISPLAY_NAME: + playerInfo.setTabListDisplayName(entry.displayName()); + } + } + + } + public EnumSet actions() { + return actions; + } + + public List npcs() { + return entries; + } + + @Override + public boolean equals(Object obj) { + if (obj == this) return true; + if (obj == null || obj.getClass() != this.getClass()) return false; + var that = (NPCInfoUpdatePacket) obj; + return Objects.equals(this.actions, that.actions) && + Objects.equals(this.entries, that.entries); + } + + @Override + public int hashCode() { + return Objects.hash(actions, entries); + } + + @Contract(pure = true) + @Override + public @NotNull String toString() { + return "NPCInfoUpdatePacket[" + + "actions=" + actions + ", " + + "npcs=" + entries + ']'; + } + + + public enum Action { + ADD_NPC((entryBuilder, buf) -> { + GameProfile gameprofile = new GameProfile(entryBuilder.profileId, buf.readUtf(16)); + gameprofile.getProperties().putAll(buf.readGameProfileProperties()); + entryBuilder.profile = gameprofile; + }, (buf, entry) -> { + buf.writeUtf(entry.profile().getName(), 16); + buf.writeGameProfileProperties(entry.profile().getProperties()); + }), + INITIALIZE_CHAT((entryBuilder, buf) -> entryBuilder.chatSession = buf.readNullable(RemoteChatSession.Data::read), (buf, entry) -> buf.writeNullable(entry.chatSession, RemoteChatSession.Data::write)), + UPDATE_GAME_MODE((entryBuilder, buf) -> entryBuilder.gameMode = GameType.byId(buf.readVarInt()), (buf, entry) -> buf.writeVarInt(entry.gameMode().getId())), + UPDATE_LISTED((entryBuilder, buf) -> entryBuilder.listed = buf.readBoolean(), (buf, entry) -> buf.writeBoolean(entry.listed())), + UPDATE_LATENCY((entryBuilder, buf) -> entryBuilder.latency = buf.readVarInt(), (buf, entry) -> buf.writeVarInt(entry.latency())), + UPDATE_DISPLAY_NAME((entryBuilder, buf) -> entryBuilder.displayName = buf.readNullable(FriendlyByteBuf::readComponent), (buf, entry) -> buf.writeNullable(entry.displayName(), FriendlyByteBuf::writeComponent)); + final Reader reader; + final Writer writer; + + Action(Reader reader, Writer writer) { + this.reader = reader; + this.writer = writer; + } + + } + + public record Entry(UUID profileId, GameProfile profile, boolean listed, int latency, GameType gameMode, + @Nullable Component displayName, @Nullable RemoteChatSession.Data chatSession) { + Entry(@NotNull NPCServerPlayer npc) { + this(npc.getUUID(), npc.getGameProfile(), true, npc.latency, npc.gameMode.getGameModeForPlayer(), npc.getTabListDisplayName(), Optionull.map(npc.getChatSession(), RemoteChatSession::asData)); + } + } + + public interface Reader { + void read(EntryBuilder entryBuilder, FriendlyByteBuf buffer); + } + + public interface Writer { + void write(FriendlyByteBuf buffer, Entry entry); + } + + public static class EntryBuilder { + final UUID profileId; + GameProfile profile; + boolean listed; + int latency; + GameType gameMode = GameType.DEFAULT_MODE; + @Nullable + Component displayName; + @Nullable + RemoteChatSession.Data chatSession; + + EntryBuilder(UUID profileId) { + this.profileId = profileId; + this.profile = new GameProfile(profileId, null); + } + + Entry build() { + return new Entry(this.profileId, this.profile, this.listed, this.latency, this.gameMode, this.displayName, this.chatSession); + } + } +} diff --git a/src/main/java/top/r3944realms/eroticdungeongame/core/storage/NPCDataStorage.java b/src/main/java/top/r3944realms/eroticdungeongame/core/storage/NPCDataStorage.java new file mode 100644 index 00000000..7cb56a56 --- /dev/null +++ b/src/main/java/top/r3944realms/eroticdungeongame/core/storage/NPCDataStorage.java @@ -0,0 +1,99 @@ +/* + * Copyright 2025-2026 R3944Realms + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package top.r3944realms.eroticdungeongame.core.storage; + +import com.mojang.datafixers.DataFixer; +import com.mojang.logging.LogUtils; +import net.minecraft.Util; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.NbtIo; +import net.minecraft.nbt.NbtUtils; +import net.minecraft.util.datafix.DataFixTypes; +import net.minecraft.world.level.storage.LevelResource; +import net.minecraft.world.level.storage.LevelStorageSource; +import org.slf4j.Logger; +import top.r3944realms.eroticdungeongame.content.entity.npc.NPCServerPlayer; + +import javax.annotation.Nullable; +import java.io.File; + +public class NPCDataStorage { + private static final Logger LOGGER = LogUtils.getLogger(); + private final File npcDir; + protected final DataFixer fixerUpper; + public static LevelResource NPC_DATA_DIR = new LevelResource("npcdata"); + public NPCDataStorage(LevelStorageSource.LevelStorageAccess levelStorageAccess, DataFixer fixerUpper) { + this.fixerUpper = fixerUpper; + this.npcDir = levelStorageAccess.getLevelPath(NPC_DATA_DIR).toFile(); + this.npcDir.mkdirs(); + } + public void save(NPCServerPlayer player) { + try { + CompoundTag compoundtag = player.saveWithoutId(new CompoundTag()); + File file1 = File.createTempFile(player.getStringUUID() + "-", ".dat", this.npcDir); + NbtIo.writeCompressed(compoundtag, file1); + File file2 = new File(this.npcDir, player.getStringUUID() + ".dat"); + File file3 = new File(this.npcDir, player.getStringUUID() + ".dat_old"); + Util.safeReplaceFile(file2, file1, file3); + net.minecraftforge.event.ForgeEventFactory.firePlayerSavingEvent(player, npcDir, player.getStringUUID()); + } catch (Exception exception) { + LOGGER.warn("Failed to save player data for {}", player.getName().getString()); + } + + } + + @Nullable + public CompoundTag load(NPCServerPlayer npcServerPlayer) { + CompoundTag compoundtag = null; + + try { + File file1 = new File(this.npcDir, npcServerPlayer.getStringUUID() + ".dat"); + if (file1.exists() && file1.isFile()) { + compoundtag = NbtIo.readCompressed(file1); + } + } catch (Exception exception) { + LOGGER.warn("Failed to load player data for {}", npcServerPlayer.getName().getString()); + } + + if (compoundtag != null) { + int i = NbtUtils.getDataVersion(compoundtag, -1); + npcServerPlayer.load(DataFixTypes.PLAYER.updateToCurrentVersion(this.fixerUpper, compoundtag, i)); + } +// net.minecraftforge.event.ForgeEventFactory.firePlayerLoadingEvent(npcServerPlayer, npcDir, npcServerPlayer.getStringUUID()); + + return compoundtag; + } + + public String[] getSeenNPCs() { + String[] astring = this.npcDir.list(); + if (astring == null) { + astring = new String[0]; + } + + for(int i = 0; i < astring.length; ++i) { + if (astring[i].endsWith(".dat")) { + astring[i] = astring[i].substring(0, astring[i].length() - 4); + } + } + + return astring; + } + + public File getNPCDataFolder() { + return npcDir; + } +} diff --git a/src/main/java/top/r3944realms/eroticdungeongame/mixin/minecraft/MixinCamera.java b/src/main/java/top/r3944realms/eroticdungeongame/mixin/minecraft/MixinCamera.java index b6ceb515..87c37a74 100644 --- a/src/main/java/top/r3944realms/eroticdungeongame/mixin/minecraft/MixinCamera.java +++ b/src/main/java/top/r3944realms/eroticdungeongame/mixin/minecraft/MixinCamera.java @@ -49,7 +49,7 @@ public class MixinCamera { target = "Lnet/minecraft/world/level/BlockGetter;clip(Lnet/minecraft/world/level/ClipContext;)Lnet/minecraft/world/phys/BlockHitResult;" ) ) - private BlockHitResult getMaxZoomFix(BlockGetter instance, ClipContext context, Operation original) { + private BlockHitResult edg$getMaxZoomFix(BlockGetter instance, ClipContext context, Operation original) { return Services.WORK_SPACE.tryToDoIfInDeviceAndRet( Minecraft.getInstance().player, data -> { diff --git a/src/main/java/top/r3944realms/eroticdungeongame/mixin/minecraft/MixinClientConnect.java b/src/main/java/top/r3944realms/eroticdungeongame/mixin/minecraft/MixinClientConnect.java new file mode 100644 index 00000000..6c98f0c4 --- /dev/null +++ b/src/main/java/top/r3944realms/eroticdungeongame/mixin/minecraft/MixinClientConnect.java @@ -0,0 +1,30 @@ +/* + * Copyright 2025-2026 R3944Realms + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package top.r3944realms.eroticdungeongame.mixin.minecraft; + +import io.netty.channel.Channel; +import net.minecraft.network.Connection; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; +import top.r3944realms.eroticdungeongame.util.IClientConnection; + +@Mixin(Connection.class) +public abstract class MixinClientConnect implements IClientConnection { + @Accessor + @Override + public abstract void setChannel(Channel var1); +} diff --git a/src/main/java/top/r3944realms/eroticdungeongame/mixin/minecraft/MixinClientPacketListener.java b/src/main/java/top/r3944realms/eroticdungeongame/mixin/minecraft/MixinClientPacketListener.java index dbffdbd8..2de6787e 100644 --- a/src/main/java/top/r3944realms/eroticdungeongame/mixin/minecraft/MixinClientPacketListener.java +++ b/src/main/java/top/r3944realms/eroticdungeongame/mixin/minecraft/MixinClientPacketListener.java @@ -16,9 +16,16 @@ package top.r3944realms.eroticdungeongame.mixin.minecraft; +import com.google.common.collect.Maps; +import com.llamalad7.mixinextras.injector.wrapmethod.WrapMethod; +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet; import net.minecraft.client.Minecraft; import net.minecraft.client.multiplayer.ClientPacketListener; +import net.minecraft.client.multiplayer.PlayerInfo; import net.minecraft.network.chat.Component; +import net.minecraft.network.protocol.PacketUtils; +import net.minecraft.network.protocol.game.ClientboundAddPlayerPacket; import net.minecraft.network.protocol.game.ClientboundSetPassengersPacket; import net.minecraft.world.entity.Entity; import org.spongepowered.asm.mixin.Final; @@ -32,26 +39,62 @@ import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; import org.spongepowered.asm.mixin.injection.callback.LocalCapture; import top.r3944realms.eroticdungeongame.client.EDGKeyBindings; import top.r3944realms.eroticdungeongame.content.entity.SeatEntity; +import top.r3944realms.eroticdungeongame.util.IEDGClientPacketListener; + +import javax.annotation.Nullable; +import java.util.Map; +import java.util.Set; +import java.util.UUID; @Mixin(ClientPacketListener.class) -public class MixinClientPacketListener { +public abstract class MixinClientPacketListener implements IEDGClientPacketListener { + @Unique + private final Map edg$npcPlayerInfoMap = Maps.newHashMap(); + @Unique + private final Set edg$listedNPCPlayers = new ReferenceOpenHashSet<>(); @Shadow @Final private Minecraft minecraft; + + @Shadow @Nullable public abstract PlayerInfo getPlayerInfo(String name); + + @Shadow @Nullable public abstract PlayerInfo getPlayerInfo(UUID uniqueId); + + @Shadow @Final private Set listedPlayers; @Unique private Entity edg$cachedMount = null; @Inject(method = "handleSetEntityPassengersPacket", locals = LocalCapture.CAPTURE_FAILEXCEPTION, at = @At(value = "INVOKE_ASSIGN", ordinal = 0, shift = At.Shift.AFTER, target = "Lnet/minecraft/client/multiplayer/ClientLevel;getEntity(I)Lnet/minecraft/world/entity/Entity;")) - private void cacheMountedEntity(ClientboundSetPassengersPacket packet, CallbackInfo ci, Entity mounted) { + private void edg$cacheMountedEntity(ClientboundSetPassengersPacket packet, CallbackInfo ci, Entity mounted) { edg$cachedMount = mounted; } @ModifyVariable(method = "handleSetEntityPassengersPacket", index = 9, at = @At(value = "INVOKE_ASSIGN", ordinal = 0, shift = At.Shift.AFTER, target = "Lnet/minecraft/network/chat/Component;translatable(Ljava/lang/String;[Ljava/lang/Object;)Lnet/minecraft/network/chat/MutableComponent;")) - private Component modifyMountMessage(Component old) { + private Component edg$modifyMountMessage(Component old) { if(edg$cachedMount != null && edg$cachedMount instanceof SeatEntity) { return Component.translatable("mount.eroticdungeongame.seat.onboard", EDGKeyBindings.KEY_QUIT.getTranslatedKeyMessage()); } else return old; } + @WrapMethod( + method = "handleAddPlayer" + ) + private void edg$handleAddPlayer(ClientboundAddPlayerPacket packet, Operation original){ + PacketUtils.ensureRunningOnSameThread(packet, ClientPacketListener.class.cast(this), this.minecraft); + PlayerInfo playerinfo = this.getPlayerInfo(packet.getPlayerId()); + if (playerinfo == null) { + + } else original.call(packet); + } + + @Override + public Map getNPCPlayerInfoMap() { + return edg$npcPlayerInfoMap; + } + + @Override + public Set getListedNPCPlayers() { + return listedPlayers; + } } diff --git a/src/main/java/top/r3944realms/eroticdungeongame/mixin/minecraft/MixinEntity.java b/src/main/java/top/r3944realms/eroticdungeongame/mixin/minecraft/MixinEntity.java index 55199beb..a522865e 100644 --- a/src/main/java/top/r3944realms/eroticdungeongame/mixin/minecraft/MixinEntity.java +++ b/src/main/java/top/r3944realms/eroticdungeongame/mixin/minecraft/MixinEntity.java @@ -19,6 +19,7 @@ package top.r3944realms.eroticdungeongame.mixin.minecraft; import com.llamalad7.mixinextras.injector.wrapmethod.WrapMethod; import com.llamalad7.mixinextras.injector.wrapoperation.Operation; import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.LivingEntity; import net.minecraft.world.entity.player.Player; import net.minecraft.world.level.Level; import net.minecraft.world.phys.AABB; @@ -28,6 +29,7 @@ import org.spongepowered.asm.mixin.Shadow; import top.r3944realms.eroticdungeongame.api.workspace.Services; import top.r3944realms.eroticdungeongame.content.block.blockentity.BaseSeatBlockEntity; import top.r3944realms.eroticdungeongame.content.entity.SeatEntity; +import top.r3944realms.eroticdungeongame.content.entity.npc.NPCServerPlayer; import top.r3944realms.eroticdungeongame.core.service.SeatService; import top.r3944realms.eroticdungeongame.util.BoundingBoxCalculator; import top.r3944realms.eroticdungeongame.util.IEDGEntity; @@ -51,10 +53,12 @@ public abstract class MixinEntity implements IEDGEntity { @Shadow public abstract void playerTouch(Player player); + @Shadow @Nullable public abstract LivingEntity getControllingPassenger(); + @WrapMethod( method = "getBoundingBox" ) - private AABB redefinedBoundingBox(@NotNull Operation original){ + private AABB edg$redefinedBoundingBox(@NotNull Operation original){ Entity self = Entity.class.cast(this); return Services.WORK_SPACE.tryToDoIfInDeviceAndRet(self, data-> { AABB playerBoundingBox = data.getPlayerBoundingBox(); @@ -67,7 +71,7 @@ public abstract class MixinEntity implements IEDGEntity { @WrapMethod( method = "startRiding(Lnet/minecraft/world/entity/Entity;Z)Z" ) - private boolean redefineStartRiding(Entity vehicle, boolean force, Operation original) { + private boolean edg$redefineStartRiding(Entity vehicle, boolean force, Operation original) { Entity entity = Entity.class.cast(this); return Services.WORK_SPACE.tryToDoIfInDeviceAndRet(entity, data -> { @@ -85,7 +89,7 @@ public abstract class MixinEntity implements IEDGEntity { @WrapMethod( method = "stopRiding" ) - private void redefineStopRiding(Operation original) { + private void edg$redefineStopRiding(Operation original) { Entity entity = Entity.class.cast(this); Services.WORK_SPACE.tryToDoIfInDevice(entity, data -> { @@ -101,6 +105,15 @@ public abstract class MixinEntity implements IEDGEntity { original::call ); } + @WrapMethod( + method = {"isControlledByLocalInstance"} + ) + private boolean isFakePlayer(Operation original) { + if (this.getControllingPassenger() instanceof NPCServerPlayer) { + return !this.level.isClientSide; + } + return original.call(); + } @SuppressWarnings("AddedMixinMembersNamePattern") diff --git a/src/main/java/top/r3944realms/eroticdungeongame/mixin/minecraft/MixinEntityRenderer.java b/src/main/java/top/r3944realms/eroticdungeongame/mixin/minecraft/MixinEntityRenderer.java index 038dc273..d6a83b05 100644 --- a/src/main/java/top/r3944realms/eroticdungeongame/mixin/minecraft/MixinEntityRenderer.java +++ b/src/main/java/top/r3944realms/eroticdungeongame/mixin/minecraft/MixinEntityRenderer.java @@ -28,7 +28,7 @@ import top.r3944realms.eroticdungeongame.api.workspace.Services; @Mixin(EntityRenderer.class) public abstract class MixinEntityRenderer { @WrapMethod(method = "getSkyLightLevel") - protected int getSkyLightLevel(T entity, BlockPos pos, Operation original) { + protected int edg$getSkyLightLevel(T entity, BlockPos pos, Operation original) { return Services.WORK_SPACE.tryToDoIfInDeviceAndRet( entity, data -> { diff --git a/src/main/java/top/r3944realms/eroticdungeongame/mixin/minecraft/MixinGameRender.java b/src/main/java/top/r3944realms/eroticdungeongame/mixin/minecraft/MixinGameRender.java index eb493268..9c8e7773 100644 --- a/src/main/java/top/r3944realms/eroticdungeongame/mixin/minecraft/MixinGameRender.java +++ b/src/main/java/top/r3944realms/eroticdungeongame/mixin/minecraft/MixinGameRender.java @@ -35,7 +35,7 @@ public class MixinGameRender { @WrapMethod( method = "renderItemInHand" ) - private void warpRenderHand(PoseStack poseStack, Camera activeRenderInfo, float partialTicks, Operation original) { + private void edg$warpRenderHand(PoseStack poseStack, Camera activeRenderInfo, float partialTicks, Operation original) { Services.WORK_SPACE.tryToDoIfInDevice(minecraft.player, data -> {}, () -> original.call(poseStack, activeRenderInfo, partialTicks)); } } diff --git a/src/main/java/top/r3944realms/eroticdungeongame/mixin/minecraft/MixinItemInHandRenderer.java b/src/main/java/top/r3944realms/eroticdungeongame/mixin/minecraft/MixinItemInHandRenderer.java index 89ff1d3a..cb9a078a 100644 --- a/src/main/java/top/r3944realms/eroticdungeongame/mixin/minecraft/MixinItemInHandRenderer.java +++ b/src/main/java/top/r3944realms/eroticdungeongame/mixin/minecraft/MixinItemInHandRenderer.java @@ -18,7 +18,7 @@ public abstract class MixinItemInHandRenderer { @WrapMethod( method = "renderItem" ) - private void removeItemInHandRender( + private void edg$removeItemInHandRender( LivingEntity entity, ItemStack itemStack, ItemDisplayContext displayContext, boolean leftHand, PoseStack poseStack, MultiBufferSource buffer, int seed, Operation original) { Services.WORK_SPACE.tryToDoIfInDevice(entity, data -> { if(data.getDeviceMainBlockPos() != null && !(entity.level().getBlockState(data.getDeviceMainBlockPos()).getBlock() instanceof DisplayRackBlock)) { diff --git a/src/main/java/top/r3944realms/eroticdungeongame/mixin/minecraft/MixinLivingEntity.java b/src/main/java/top/r3944realms/eroticdungeongame/mixin/minecraft/MixinLivingEntity.java index 12fc69c5..74f9e0c7 100644 --- a/src/main/java/top/r3944realms/eroticdungeongame/mixin/minecraft/MixinLivingEntity.java +++ b/src/main/java/top/r3944realms/eroticdungeongame/mixin/minecraft/MixinLivingEntity.java @@ -46,14 +46,14 @@ public abstract class MixinLivingEntity extends Entity { target= "Lnet/minecraft/world/entity/LivingEntity;isInWall()Z" ) ) - private boolean inWall$Warp(LivingEntity instance, Operation original) { + private boolean edg$inWall$Warp(LivingEntity instance, Operation original) { return Services.WORK_SPACE.tryToDoIfInDeviceAndRet(instance, data -> false, () -> original.call(instance)); } @WrapMethod( method = "stopRiding" ) - private void redefineStopRiding(Operation original) { + private void edg$redefineStopRiding(Operation original) { LivingEntity livingEntity = LivingEntity.class.cast(this); Services.WORK_SPACE.tryToDoIfInDevice(livingEntity, data -> { diff --git a/src/main/java/top/r3944realms/eroticdungeongame/mixin/minecraft/MixinMinecraft.java b/src/main/java/top/r3944realms/eroticdungeongame/mixin/minecraft/MixinMinecraft.java index 1fa455d2..2c7d8f21 100644 --- a/src/main/java/top/r3944realms/eroticdungeongame/mixin/minecraft/MixinMinecraft.java +++ b/src/main/java/top/r3944realms/eroticdungeongame/mixin/minecraft/MixinMinecraft.java @@ -35,21 +35,21 @@ public class MixinMinecraft { @WrapMethod( method = "startAttack" ) - private boolean forbiddenAttack(Operation original){ + private boolean edg$forbiddenAttack(Operation original){ return Services.WORK_SPACE.tryToDoIfInDeviceAndRet(player, data -> false, original::call); } @WrapMethod( method = "startUseItem" ) - private void forbiddenUseItem(Operation original){ + private void edg$forbiddenUseItem(Operation original){ Services.WORK_SPACE.tryToDoIfInDevice(player, data -> {}, original::call); } @WrapMethod( method = "continueAttack" ) - private void forbiddenContinueAttack(boolean leftClick, Operation original){ + private void edg$forbiddenContinueAttack(boolean leftClick, Operation original){ Services.WORK_SPACE.tryToDoIfInDeviceAndRet(player, data -> false, () -> original.call(leftClick)); } } diff --git a/src/main/java/top/r3944realms/eroticdungeongame/mixin/minecraft/MixinMinecraftServer.java b/src/main/java/top/r3944realms/eroticdungeongame/mixin/minecraft/MixinMinecraftServer.java new file mode 100644 index 00000000..1fd541a9 --- /dev/null +++ b/src/main/java/top/r3944realms/eroticdungeongame/mixin/minecraft/MixinMinecraftServer.java @@ -0,0 +1,88 @@ +/* + * Copyright 2025-2026 R3944Realms + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package top.r3944realms.eroticdungeongame.mixin.minecraft; + +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; +import net.minecraft.core.RegistryAccess; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.players.PlayerList; +import net.minecraft.world.level.storage.LevelStorageSource; +import net.minecraft.world.level.storage.WorldData; +import org.jetbrains.annotations.NotNull; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import top.r3944realms.eroticdungeongame.content.entity.npc.NPCPlayerList; +import top.r3944realms.eroticdungeongame.util.IEDGMinecraftServer; + +@Mixin(MinecraftServer.class) +public class MixinMinecraftServer implements IEDGMinecraftServer { + + @Unique + private NPCPlayerList edg$npcPlayerList; + + @SuppressWarnings("AddedMixinMembersNamePattern") + @Override + public NPCPlayerList getNPCPlayerList() { + return edg$npcPlayerList; + } + + @SuppressWarnings("AddedMixinMembersNamePattern") + @Override + public void setNPCPlayerList(NPCPlayerList playerList) { + this.edg$npcPlayerList = playerList; + } + @WrapOperation( + method = "saveEverything", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/server/players/PlayerList;saveAll()V" + ) + ) + void edg$saveEverything$saveAll(PlayerList instance, @NotNull Operation original) { + original.call(instance); + edg$npcPlayerList.saveAll(); + } + @WrapOperation( + method = "stopServer", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/server/players/PlayerList;saveAll()V" + ) + ) + void edg$stopServer$saveAll(PlayerList instance, @NotNull Operation original) { + original.call(instance); + edg$npcPlayerList.saveAll(); + edg$npcPlayerList.removeAll(); + } + + @WrapOperation( + method = "lambda$reloadResources$26", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/server/players/PlayerList;reloadResources()V" + ) + ) + void edg$reloadResources$saveAll(PlayerList instance, @NotNull Operation original) { + original.call(instance); + edg$npcPlayerList.saveAll(); + edg$npcPlayerList.removeAll(); + } + +} diff --git a/src/main/java/top/r3944realms/eroticdungeongame/mixin/minecraft/MixinPlayer.java b/src/main/java/top/r3944realms/eroticdungeongame/mixin/minecraft/MixinPlayer.java index eaa08e92..473778bf 100644 --- a/src/main/java/top/r3944realms/eroticdungeongame/mixin/minecraft/MixinPlayer.java +++ b/src/main/java/top/r3944realms/eroticdungeongame/mixin/minecraft/MixinPlayer.java @@ -18,28 +18,59 @@ package top.r3944realms.eroticdungeongame.mixin.minecraft; import com.llamalad7.mixinextras.injector.wrapmethod.WrapMethod; import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; import net.minecraft.world.entity.EntityType; import net.minecraft.world.entity.LivingEntity; import net.minecraft.world.entity.player.Player; import net.minecraft.world.level.Level; +import org.jetbrains.annotations.NotNull; import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Redirect; import top.r3944realms.eroticdungeongame.api.workspace.Services; +import top.r3944realms.eroticdungeongame.content.entity.npc.INPCPlayer; + +import java.util.UUID; @Mixin(Player.class) public abstract class MixinPlayer extends LivingEntity { protected MixinPlayer(EntityType entityType, Level level) { super(entityType, level); } - + @Redirect( + method = "", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/world/entity/player/Player;setUUID(Ljava/util/UUID;)V" + ) + ) + private void edg$Player$setUUID(Player instance, UUID uuid) { + if (!(instance instanceof INPCPlayer)) { + instance.setUUID(uuid); + } + } @WrapMethod(method = "shouldShowName") - public boolean shouldShowName(Operation original) { + public boolean edg$shouldShowName(@NotNull Operation original) { Player player = Player.class.cast(this); return Services.WORK_SPACE.tryToDoIfInDeviceAndRet(player, data -> false, original::call); } @WrapMethod(method = "wantsToStopRiding") - private boolean wantToStopRide(Operation original) { + private boolean edg$wantToStopRide(@NotNull Operation original) { Player player = Player.class.cast(this); return Services.WORK_SPACE.tryToDoIfInDeviceAndRet(player, data -> false, original::call); } + @WrapOperation( + method = "readAdditionalSaveData", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/world/entity/player/Player;setUUID(Ljava/util/UUID;)V" + ) + ) + private void edg$readAdditionalSaveData$setUUID(Player instance, UUID uuid, Operation original) { + if (!(instance instanceof INPCPlayer)) { + original.call(instance, uuid); + } + } + } diff --git a/src/main/java/top/r3944realms/eroticdungeongame/mixin/minecraft/MixinPlayerRenderer.java b/src/main/java/top/r3944realms/eroticdungeongame/mixin/minecraft/MixinPlayerRenderer.java index 523ade00..95e34d86 100644 --- a/src/main/java/top/r3944realms/eroticdungeongame/mixin/minecraft/MixinPlayerRenderer.java +++ b/src/main/java/top/r3944realms/eroticdungeongame/mixin/minecraft/MixinPlayerRenderer.java @@ -38,7 +38,7 @@ public class MixinPlayerRenderer { @WrapMethod( method = "renderNameTag(Lnet/minecraft/client/player/AbstractClientPlayer;Lnet/minecraft/network/chat/Component;Lcom/mojang/blaze3d/vertex/PoseStack;Lnet/minecraft/client/renderer/MultiBufferSource;I)V" ) - private void renderNameTag(AbstractClientPlayer entity, Component displayName, PoseStack poseStack, MultiBufferSource buffer, int packedLight, Operation original) { + private void edg$renderNameTag(AbstractClientPlayer entity, Component displayName, PoseStack poseStack, MultiBufferSource buffer, int packedLight, Operation original) { Services.WORK_SPACE.tryToDoIfInDevice(entity, data -> {}, () -> original.call(entity, displayName, poseStack, buffer, packedLight)); } @@ -50,7 +50,7 @@ public class MixinPlayerRenderer { ) ) - private void renderFix(PlayerRenderer instance, T entity, float entityYaw, float partialTicks, PoseStack poseStack, MultiBufferSource buffer, int packedLight, Operation original) { + private void edg$renderFix(PlayerRenderer instance, T entity, float entityYaw, float partialTicks, PoseStack poseStack, MultiBufferSource buffer, int packedLight, Operation original) { Services.WORK_SPACE.tryToDoIfInDevice(entity, data -> { Minecraft minecraft = Minecraft.getInstance(); if(!(Objects.equals(minecraft.player, entity) && minecraft.options.getCameraType().isFirstPerson() && diff --git a/src/main/java/top/r3944realms/eroticdungeongame/mixin/minecraft/MixinServerPlayer.java b/src/main/java/top/r3944realms/eroticdungeongame/mixin/minecraft/MixinServerPlayer.java index 887b15d5..47cb7b6c 100644 --- a/src/main/java/top/r3944realms/eroticdungeongame/mixin/minecraft/MixinServerPlayer.java +++ b/src/main/java/top/r3944realms/eroticdungeongame/mixin/minecraft/MixinServerPlayer.java @@ -57,7 +57,7 @@ public abstract class MixinServerPlayer extends Player implements IEDGEntity { @WrapMethod( method = "stopRiding" ) - private void redefineStopRiding(Operation original) { + private void edg$redefineStopRiding(Operation original) { ServerPlayer player = ServerPlayer.class.cast(this); Services.WORK_SPACE.tryToDoIfInDevice(player, data -> { diff --git a/src/main/java/top/r3944realms/eroticdungeongame/util/IClientConnection.java b/src/main/java/top/r3944realms/eroticdungeongame/util/IClientConnection.java new file mode 100644 index 00000000..a81d3733 --- /dev/null +++ b/src/main/java/top/r3944realms/eroticdungeongame/util/IClientConnection.java @@ -0,0 +1,23 @@ +/* + * Copyright 2025-2026 R3944Realms + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package top.r3944realms.eroticdungeongame.util; + +import io.netty.channel.Channel; + +public interface IClientConnection { + void setChannel(Channel var1); +} diff --git a/src/main/java/top/r3944realms/eroticdungeongame/util/IEDGClientPacketListener.java b/src/main/java/top/r3944realms/eroticdungeongame/util/IEDGClientPacketListener.java new file mode 100644 index 00000000..d851c9b5 --- /dev/null +++ b/src/main/java/top/r3944realms/eroticdungeongame/util/IEDGClientPacketListener.java @@ -0,0 +1,49 @@ +/* + * Copyright 2025-2026 R3944Realms + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package top.r3944realms.eroticdungeongame.util; + +import net.minecraft.client.multiplayer.PlayerInfo; + +import javax.annotation.Nullable; +import java.util.Collection; +import java.util.Map; +import java.util.Set; +import java.util.UUID; + +public interface IEDGClientPacketListener { + Map getNPCPlayerInfoMap(); + Set getListedNPCPlayers(); + default Collection getOnlineNPCPlayers() { + return getNPCPlayerInfoMap().values(); + } + default Collection getOnlineNPCPlayerIds() { + return getNPCPlayerInfoMap().keySet(); + } + @Nullable + default PlayerInfo getNPCPlayerInfo(UUID uniqueId) { + return getNPCPlayerInfoMap().get(uniqueId); + } + @Nullable + default PlayerInfo getNPCPlayerInfo(String name) { + for(PlayerInfo playerinfo : getNPCPlayerInfoMap().values()) { + if (playerinfo.getProfile().getName().equals(name)) { + return playerinfo; + } + } + return null; + } +} diff --git a/src/main/java/top/r3944realms/eroticdungeongame/util/IEDGMinecraftServer.java b/src/main/java/top/r3944realms/eroticdungeongame/util/IEDGMinecraftServer.java new file mode 100644 index 00000000..96bc1bf2 --- /dev/null +++ b/src/main/java/top/r3944realms/eroticdungeongame/util/IEDGMinecraftServer.java @@ -0,0 +1,24 @@ +/* + * Copyright 2025-2026 R3944Realms + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package top.r3944realms.eroticdungeongame.util; + +import top.r3944realms.eroticdungeongame.content.entity.npc.NPCPlayerList; + +public interface IEDGMinecraftServer { + NPCPlayerList getNPCPlayerList(); + void setNPCPlayerList(NPCPlayerList playerList); +} diff --git a/src/main/resources/eroticdungeongame.mixins.json b/src/main/resources/eroticdungeongame.mixins.json index c56dddd7..ccad17c9 100644 --- a/src/main/resources/eroticdungeongame.mixins.json +++ b/src/main/resources/eroticdungeongame.mixins.json @@ -7,9 +7,12 @@ "refmap": "eroticdungeongame.refmap.json", "mixins": [ "bendylib.MixinBendableCuboidBuilder", + "minecraft.MixinClientConnect", "minecraft.MixinEntity", "minecraft.MixinLivingEntity", + "minecraft.MixinMinecraftServer", "minecraft.MixinPlayer", + "minecraft.MixinServerLoginPacketListenerImpl", "minecraft.MixinServerPlayer" ], "client": [ diff --git a/src/main/templates/META-INF/accesstransformer.cfg b/src/main/templates/META-INF/accesstransformer.cfg index 0948f5a5..25228c67 100644 --- a/src/main/templates/META-INF/accesstransformer.cfg +++ b/src/main/templates/META-INF/accesstransformer.cfg @@ -40,4 +40,9 @@ protected net.minecraft.client.gui.screens.recipebook.RecipeButton f_100467_ # a protected net.minecraft.client.gui.screens.recipebook.RecipeButton f_100468_ # currentIndex protected net.minecraft.client.gui.screens.recipebook.RecipeButton m_100490_()Ljava/util/List; # getOrderedRecipes public net.minecraft.world.entity.Entity f_19816_ # eyeHeight -public net.minecraft.client.Camera m_90581_(Lnet/minecraft/world/phys/Vec3;)V # setPosition \ No newline at end of file +public net.minecraft.client.Camera m_90581_(Lnet/minecraft/world/phys/Vec3;)V # setPosition +public net.minecraft.client.multiplayer.ClientPacketListener m_245842_(Lnet/minecraft/network/protocol/game/ClientboundPlayerInfoUpdatePacket$Entry;Lnet/minecraft/client/multiplayer/PlayerInfo;)V # initializeChatSession +public net.minecraft.client.multiplayer.ClientPacketListener f_244156_ # listedPlayers +public net.minecraft.client.multiplayer.PlayerInfo m_105317_(Lnet/minecraft/world/level/GameType;)V # setGameMode +public net.minecraft.client.multiplayer.PlayerInfo m_105313_(I)V # setLatency +public net.minecraft.client.multiplayer.ClientPacketListener f_104892_ # playerInfoMap \ No newline at end of file