feat: nPC进一步完善,自动加载,杀死删除

目前问题是创建退出世界后在加入单人看不见该NPC,需远离跟踪范围后才能重新看见(暂时未排查出具体原因,不过问题应该定位在ChunkMap这里)
This commit is contained in:
叁玖领域 2026-03-22 00:31:48 +08:00
parent f1f5e204a2
commit 872c69f4fe
21 changed files with 1068 additions and 81 deletions

View File

@ -32,7 +32,7 @@ emi_version=1.1.22+1.20.1
player_anim_version=1.0.2-rc1+1.20
geckolib_version=4.2.1
curios_version=5.5.0+1.20.1
lib39_version=0.5.5
lib39_version=0.5.6
accore_version=1.20.1-26H7
super_lead_version=1.20.1-1.2.1
## Mod Properties

View File

@ -0,0 +1,383 @@
/*
* 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.Lists;
import com.google.common.collect.Maps;
import com.mojang.authlib.GameProfile;
import net.minecraft.commands.CommandSourceStack;
import net.minecraft.core.LayeredRegistryAccess;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.network.Connection;
import net.minecraft.network.chat.ChatType;
import net.minecraft.network.chat.Component;
import net.minecraft.network.chat.PlayerChatMessage;
import net.minecraft.network.protocol.Packet;
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.players.*;
import net.minecraft.stats.ServerStatsCounter;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.level.Level;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import top.r3944realms.eroticdungeongame.EroticDungeon;
import java.net.SocketAddress;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.function.Function;
public class FakePlayerList extends PlayerList {
private final List<NPCServerPlayer> npcs = new ArrayList<>();
private final Map<UUID, NPCServerPlayer> npcsByUUID = Maps.newHashMap();
private int sendAllNPCInfoIn;
private final List<ServerPlayer> npcsView = java.util.Collections.unmodifiableList(npcs);
@SuppressWarnings("DataFlowIssue")
public FakePlayerList(MinecraftServer server, LayeredRegistryAccess<RegistryLayer> registries, List<NPCServerPlayer> npcs, Map<UUID, NPCServerPlayer> npcsByUUID) {
super(server, registries, null, -1);
this.npcs.addAll(npcs);
this.npcsByUUID.putAll(npcsByUUID);
}
private static void noSupportOperate() {
EroticDungeon.getLogger().error("Not supported in FakePlayerList");
}
@Override
public void placeNewPlayer(@NotNull Connection netManager, @NotNull ServerPlayer player) {
noSupportOperate();
}
@Override
protected void updateEntireScoreboard(@NotNull ServerScoreboard scoreboard, @NotNull ServerPlayer player) {
noSupportOperate();
}
@Override
public void addWorldborderListener(@NotNull ServerLevel level) {
noSupportOperate();
}
@Nullable
@Override
public CompoundTag load(@NotNull ServerPlayer player) {
noSupportOperate();
return null;
}
@Override
protected void save(@NotNull ServerPlayer player) {
noSupportOperate();
}
@Override
public void remove(@NotNull ServerPlayer player) {
noSupportOperate();
}
@Nullable
@Override
public Component canPlayerLogin(@NotNull SocketAddress socketAddress, @NotNull GameProfile gameProfile) {
noSupportOperate();
return null;
}
@Override
public @NotNull ServerPlayer getPlayerForLogin(@NotNull GameProfile profile) {
noSupportOperate();
return super.getPlayerForLogin(profile);
}
@Override
public @NotNull ServerPlayer respawn(@NotNull ServerPlayer player, boolean keepEverything) {
noSupportOperate();
return super.respawn(player, keepEverything);
}
@Override
public void sendPlayerPermissionLevel(@NotNull ServerPlayer player) {
noSupportOperate();
}
@Override
public void tick() {
noSupportOperate();
}
@Override
public void broadcastAll(@NotNull Packet<?> packet) {
noSupportOperate();
}
@Override
public void broadcastAll(@NotNull Packet<?> packet, @NotNull ResourceKey<Level> dimension) {
noSupportOperate();
}
@Override
public void broadcastSystemToTeam(@NotNull Player player, @NotNull Component message) {
noSupportOperate();
}
@Override
public void broadcastSystemToAllExceptTeam(@NotNull Player player, @NotNull Component message) {
noSupportOperate();
}
@Override
public String @NotNull [] getPlayerNamesArray() {
noSupportOperate();
return super.getPlayerNamesArray();
}
@Override
public @NotNull UserBanList getBans() {
noSupportOperate();
return super.getBans();
}
@Override
public @NotNull IpBanList getIpBans() {
noSupportOperate();
return super.getIpBans();
}
@Override
public void op(@NotNull GameProfile profile) {
noSupportOperate();
}
@Override
public void deop(@NotNull GameProfile profile) {
noSupportOperate();
}
@Override
public boolean isWhiteListed(@NotNull GameProfile profile) {
noSupportOperate();
return super.isWhiteListed(profile);
}
@Override
public boolean isOp(@NotNull GameProfile profile) {
noSupportOperate();
return super.isOp(profile);
}
@Nullable
@Override
public ServerPlayer getPlayerByName(@NotNull String username) {
return super.getPlayerByName(username);
}
@Override
public void broadcast(@Nullable Player except, double x, double y, double z, double radius, @NotNull ResourceKey<Level> dimension, @NotNull Packet<?> packet) {
noSupportOperate();
}
@Override
public void saveAll() {
noSupportOperate();
}
@Override
public @NotNull UserWhiteList getWhiteList() {
noSupportOperate();
return super.getWhiteList();
}
@Override
public String @NotNull [] getWhiteListNames() {
noSupportOperate();
return super.getWhiteListNames();
}
@Override
public @NotNull ServerOpList getOps() {
noSupportOperate();
return super.getOps();
}
@Override
public String @NotNull [] getOpNames() {
noSupportOperate();
return super.getOpNames();
}
@Override
public void reloadWhiteList() {
noSupportOperate();
}
@Override
public void sendLevelInfo(@NotNull ServerPlayer player, @NotNull ServerLevel level) {
noSupportOperate();
}
@Override
public void sendAllPlayerInfo(@NotNull ServerPlayer player) {
noSupportOperate();
}
@Override
public int getPlayerCount() {
return this.npcs.size();
}
@Override
public int getMaxPlayers() {
noSupportOperate();
return super.getMaxPlayers();
}
@Override
public boolean isUsingWhitelist() {
noSupportOperate();
return super.isUsingWhitelist();
}
@Override
public void setUsingWhiteList(boolean whitelistEnabled) {
noSupportOperate();
super.setUsingWhiteList(whitelistEnabled);
}
@Override
public @NotNull List<ServerPlayer> getPlayersWithAddress(@NotNull String address) {
List<ServerPlayer> list = Lists.newArrayList();
for(ServerPlayer serverplayer : this.npcs) {
if (serverplayer.getIpAddress().equals(address)) {
list.add(serverplayer);
}
}
return list;
}
@Override
public int getViewDistance() {
noSupportOperate();
return super.getViewDistance();
}
@Override
public int getSimulationDistance() {
noSupportOperate();
return super.getSimulationDistance();
}
@Override
public @NotNull MinecraftServer getServer() {
return super.getServer();
}
@Nullable
@Override
public CompoundTag getSingleplayerData() {
noSupportOperate();
return null;
}
@Override
public void setAllowCheatsForAllPlayers(boolean allowCheatsForAllPlayers) {
noSupportOperate();
}
@Override
public void removeAll() {
noSupportOperate();
}
@Override
public void broadcastSystemMessage(@NotNull Component message, boolean bypassHiddenChat) {
noSupportOperate();
}
@Override
public void broadcastSystemMessage(@NotNull Component serverMessage, @NotNull Function<ServerPlayer, Component> playerMessageFactory, boolean bypassHiddenChat) {
noSupportOperate();
}
@Override
public void broadcastChatMessage(@NotNull PlayerChatMessage message, @NotNull CommandSourceStack sender, ChatType.@NotNull Bound boundChatType) {
noSupportOperate();
}
@Override
public void broadcastChatMessage(@NotNull PlayerChatMessage message, @NotNull ServerPlayer sender, ChatType.@NotNull Bound boundChatType) {
noSupportOperate();
}
@Override
public @NotNull ServerStatsCounter getPlayerStats(@NotNull Player player) {
noSupportOperate();
return super.getPlayerStats(player);
}
@Override
public @NotNull PlayerAdvancements getPlayerAdvancements(@NotNull ServerPlayer player) {
noSupportOperate();
return super.getPlayerAdvancements(player);
}
@Override
public void setViewDistance(int viewDistance) {
noSupportOperate();
}
@Override
public void setSimulationDistance(int simulationDistance) {
noSupportOperate();
}
@Override
public @NotNull List<ServerPlayer> getPlayers() {
return this.npcsView;
}
@Nullable
@Override
public ServerPlayer getPlayer(@NotNull UUID playerUUID) {
return npcsByUUID.get(playerUUID);
}
@Override
public boolean canBypassPlayerLimit(@NotNull GameProfile profile) {
noSupportOperate();
return super.canBypassPlayerLimit(profile);
}
@Override
public void reloadResources() {
noSupportOperate();
}
@Override
public boolean isAllowCheatsForAllPlayers() {
noSupportOperate();
return super.isAllowCheatsForAllPlayers();
}
}

View File

@ -23,21 +23,23 @@ import com.mojang.logging.LogUtils;
import com.mojang.serialization.Dynamic;
import io.netty.buffer.Unpooled;
import net.minecraft.ChatFormatting;
import net.minecraft.Util;
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.nbt.NbtUtils;
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.PacketFlow;
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;
@ -45,7 +47,6 @@ 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;
@ -60,23 +61,26 @@ 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 net.minecraftforge.common.MinecraftForge;
import net.minecraftforge.event.OnDatapackSyncEvent;
import org.jetbrains.annotations.NotNull;
import org.slf4j.Logger;
import top.r3944realms.eroticdungeongame.core.network.EDGNetworkHandler;
import top.r3944realms.eroticdungeongame.core.network.NPCEmptyClientConnection;
import top.r3944realms.eroticdungeongame.core.network.toClient.NPCInfoUpdatePacket;
import top.r3944realms.eroticdungeongame.core.storage.NPCDataStorage;
import top.r3944realms.lib39.util.nbt.NBTReader;
import javax.annotation.Nullable;
import java.text.SimpleDateFormat;
import java.time.Instant;
import java.util.*;
import java.util.concurrent.atomic.AtomicReference;
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<RegistryLayer> registries;
private final RegistryAccess.Frozen synchronizedRegistries;
private final NPCDataStorage npcIo;
@ -86,13 +90,13 @@ public class NPCPlayerList {
private int sendAllNPCInfoIn;
private final List<NPCServerPlayer> npcsView = java.util.Collections.unmodifiableList(npcs);
public static final Component CHAT_FILTERED_FULL = Component.translatable("chat.filtered_full");
public NPCPlayerList(MinecraftServer server, LayeredRegistryAccess<RegistryLayer> registries, NPCDataStorage npcDataStorage, int maxPlayers) {
public NPCPlayerList(MinecraftServer server, LayeredRegistryAccess<RegistryLayer> registries, NPCDataStorage npcDataStorage) {
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) {
public void placeNewNPC(Connection netManager, @NotNull NPCServerPlayer npc) {
GameProfile gameprofile = npc.getGameProfile();
GameProfileCache gameprofilecache = this.server.getProfileCache();
String s;
@ -106,14 +110,7 @@ public class NPCPlayerList {
CompoundTag compoundtag = this.load(npc);
//noinspection deprecation
ResourceKey<Level> 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;
}
ServerLevel serverlevel1 = getServerLevel(resourcekey);
npc.setServerLevel(serverlevel1);
String s1 = "local";
@ -135,8 +132,10 @@ public class NPCPlayerList {
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));
//数据同步事件对于NPC类 搞个继承类复用这个事件 (如果有模组用这个PlayerList搞其它事情可能会导致BUG)
MinecraftForge.EVENT_BUS.post(new OnDatapackSyncEvent(this.toFakePlayerList(), npc));
servergamepacketlistenerimpl.send(new ClientboundUpdateRecipesPacket(this.server.getRecipeManager().getRecipes()));
servergamepacketlistenerimpl.send(new ClientboundUpdateTagsPacket(TagNetworkSerialization.serializeTagsToNetwork(this.registries)));
this.sendPlayerPermissionLevel(npc);
@ -145,7 +144,7 @@ public class NPCPlayerList {
this.updateEntireScoreboard(serverlevel1.getScoreboard(), npc);
this.server.invalidateStatus();
MutableComponent mutablecomponent;
if (npc.getGameProfile().getName().equalsIgnoreCase(s)) { //todo npc
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);
@ -157,7 +156,7 @@ public class NPCPlayerList {
if (serverstatus != null) {
npc.sendServerStatus(serverstatus);
}
//todo 实现网络传递
// 实现网络传递
EDGNetworkHandler.sendToPlayer(NPCInfoUpdatePacket.createNPCPlayerInitializing(this.npcs), npc);
this.npcs.add(npc);
this.npcsByUUID.put(npc.getUUID(), npc);
@ -207,6 +206,79 @@ public class NPCPlayerList {
npc.initInventoryMenu();
net.minecraftforge.event.ForgeEventFactory.firePlayerLoggedIn(npc);
}
private @NotNull ServerLevel getServerLevel(ResourceKey<Level> resourcekey) {
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;
}
return serverlevel1;
}
/**
* 加载所有已存在的 NPC从磁盘
*/
public void loadAllNPCs() {
String[] seenNPCs = this.npcIo.getSeenNPCs();
LOGGER.info("Loading {} existing NPCs from disk", seenNPCs.length);
for (String uuidStr : seenNPCs) {
try {
UUID uuid = UUID.fromString(uuidStr);
if (loadNPC(uuid)) {
LOGGER.info("Loaded NPC {} from disk", uuidStr);
}
} catch (Exception e) {
LOGGER.error("Failed to load NPC with UUID: {}", uuidStr, e);
}
}
}
/**
* 加载指定的 NPC
* @param uuid NPC UUID
* @return 是否加载成功
*/
public boolean loadNPC(UUID uuid) {
// 检查是否已经加载
if (this.npcsByUUID.containsKey(uuid)) {
LOGGER.debug("NPC {} already loaded", uuid);
return true;
}
try {
NPCServerPlayer placeholder = new NPCServerPlayer(server, server.overworld(), new GameProfile(Util.NIL_UUID, "placeholder"));
placeholder.setUUID(uuid);
NPCServerPlayer npcServerPlayer = this.npcIo.load(uuid, this::createNPCFromData);
if (npcServerPlayer != null) {
placeNewNPC(new NPCEmptyClientConnection(PacketFlow.SERVERBOUND), npcServerPlayer);
return true;
}
} catch (Exception e) {
LOGGER.error("Error loading NPC with UUID: {}", uuid, e);
}
return false;
}
public @NotNull NPCServerPlayer createNPCFromData(CompoundTag npcData) {
AtomicReference<String> npcName = new AtomicReference<>();
AtomicReference<GameProfile> gameProfile = new AtomicReference<>();
AtomicReference<ResourceKey<Level>> dimension = new AtomicReference<>(Level.OVERWORLD);
//noinspection deprecation
NBTReader.of(npcData)
.string("NpcName", npcName::set)
.compound("NpcGameProfile", data -> gameProfile.set(NbtUtils.readGameProfile(data)))
.compound("Dimension", data -> dimension.set(DimensionType.parseLegacy(new Dynamic<>(NbtOps.INSTANCE, data.get("Dimension")))
.resultOrPartial(LOGGER::error)
.orElse(Level.OVERWORLD)));
ResourceKey<Level> levelResourceKey = dimension.get();
ServerLevel serverlevel1 = getServerLevel(levelResourceKey);
return new NPCServerPlayer(server, serverlevel1, gameProfile.get());
}
public void sendPlayerPermissionLevel(@NotNull ServerPlayer player) {
GameProfile gameprofile = player.getGameProfile();
int i = this.server.getProfilePermissions(gameprofile);
@ -265,16 +337,17 @@ public class NPCPlayerList {
* Called during player login. Reads the player information from disk.
*/
@Nullable
public CompoundTag load(@NotNull NPCServerPlayer player) {
public CompoundTag load(@NotNull NPCServerPlayer npc) {
CompoundTag compoundtag = this.server.getWorldData().getLoadedPlayerTag();
CompoundTag compoundtag1;
if (this.server.isSingleplayerOwner(player.getGameProfile()) && compoundtag != null) {
if (this.server.isSingleplayerOwner(npc.getGameProfile()) && compoundtag != null) {
compoundtag1 = compoundtag;
player.load(compoundtag);
npc.load(compoundtag);
LOGGER.debug("loading single npc");
// net.minecraftforge.event.ForgeEventFactory.firePlayerLoadingEvent(player, this.npcIo, player.getUUID().toString());
// 应该不会有模组干这个吧
net.minecraftforge.event.ForgeEventFactory.firePlayerLoadingEvent(npc, this.npcIo.getNPCDataFolder(), npc.getUUID().toString());
} else {
compoundtag1 = this.npcIo.load(player);
compoundtag1 = this.npcIo.load(npc);
}
return compoundtag1;
@ -298,6 +371,11 @@ public class NPCPlayerList {
}
public void delete(NPCServerPlayer npc) {
remove(npc);
npcIo.deleteNPCData(npc);
}
public void remove(NPCServerPlayer npc) {
net.minecraftforge.event.ForgeEventFactory.firePlayerLoggedOut(npc);
ServerLevel serverlevel = npc.serverLevel();
@ -324,7 +402,6 @@ public class NPCPlayerList {
if (serverplayer == npc) {
this.npcsByUUID.remove(uuid);
}
this.broadcastAll(new ClientboundPlayerInfoRemovePacket(List.of(npc.getUUID())));
}
@ -347,7 +424,7 @@ public class NPCPlayerList {
}
public void tick() {
if (++this.sendAllNPCInfoIn > 600) {
if (++this.sendAllNPCInfoIn > SEND_PLAYER_INFO_INTERVAL) {
EDGNetworkHandler.sendToAllPlayer(new NPCInfoUpdatePacket(EnumSet.of(NPCInfoUpdatePacket.Action.UPDATE_LATENCY), this.npcs));
this.sendAllNPCInfoIn = 0;
}
@ -434,4 +511,8 @@ public class NPCPlayerList {
public MinecraftServer getServer() {
return this.server;
}
public @NotNull FakePlayerList toFakePlayerList() {
return new FakePlayerList(this.server, this.registries, this.npcs, this.npcsByUUID);
}
}

View File

@ -17,11 +17,27 @@
package top.r3944realms.eroticdungeongame.content.entity.npc;
import com.mojang.authlib.GameProfile;
import net.minecraft.client.Minecraft;
import net.minecraft.client.multiplayer.ClientLevel;
import net.minecraft.client.multiplayer.PlayerInfo;
import net.minecraft.client.player.RemotePlayer;
import org.jetbrains.annotations.Nullable;
import top.r3944realms.eroticdungeongame.util.IEDGClientPacketListener;
public class NPCRemotePlayer extends RemotePlayer implements INPCPlayer {
private PlayerInfo npcPlayerInfo;
public NPCRemotePlayer(ClientLevel clientLevel, GameProfile gameProfile) {
super(clientLevel, gameProfile);
}
@Nullable
@Override
protected PlayerInfo getPlayerInfo() {
if (this.npcPlayerInfo == null) {
if (Minecraft.getInstance().getConnection() instanceof IEDGClientPacketListener iedgClientPacketListener) {
this.npcPlayerInfo = iedgClientPacketListener.getNPCPlayerInfo(this.getUUID());
}
}
return this.npcPlayerInfo;
}
}

View File

@ -21,11 +21,11 @@ import dev.dubhe.curtain.utils.Messenger;
import net.minecraft.core.BlockPos;
import net.minecraft.core.UUIDUtil;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.NbtUtils;
import net.minecraft.network.chat.Component;
import net.minecraft.network.protocol.Packet;
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.network.protocol.game.*;
import net.minecraft.resources.ResourceKey;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.TickTask;
@ -46,12 +46,24 @@ import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import top.r3944realms.eroticdungeongame.EroticDungeon;
import top.r3944realms.eroticdungeongame.core.network.NPCEmptyClientConnection;
import top.r3944realms.eroticdungeongame.util.IEDGMinecraftServer;
import top.r3944realms.lib39.util.nbt.NBTWriter;
import java.util.Iterator;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicReference;
public class NPCServerPlayer extends ServerPlayer implements INPCPlayer {
public static final String PREFIX = "[NPC]";
public String npcNameWithoutPrefix;
public String getNpcNameWithoutPrefix() {
return npcNameWithoutPrefix;
}
public void setNpcNameWithoutPrefix(String npcNameWithoutPrefix) {
this.npcNameWithoutPrefix = npcNameWithoutPrefix;
}
public Runnable fixStartingPosition = () -> {
};
public NPCServerPlayer(MinecraftServer server, ServerLevel level, GameProfile gameProfile) {
@ -86,14 +98,14 @@ public class NPCServerPlayer extends ServerPlayer implements INPCPlayer {
instance.fixStartingPosition = () -> {
instance.moveTo(x, y, z, (float) yaw, (float) pitch);
};
server.getPlayerList().placeNewPlayer(new NPCEmptyClientConnection(PacketFlow.SERVERBOUND), instance);
((IEDGMinecraftServer)server).getNPCPlayerList().placeNewNPC(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);
((IEDGMinecraftServer)server).getNPCPlayerList().broadcastAll(new ClientboundRotateHeadPacket(instance, (byte)((int)(instance.yHeadRot * 256.0F / 360.0F))), dimensionId);
((IEDGMinecraftServer)server).getNPCPlayerList().broadcastAll(new ClientboundTeleportEntityPacket(instance), dimensionId);
instance.entityData.set(DATA_PLAYER_MODE_CUSTOMISATION, (byte)127);
instance.getAbilities().flying = isflying;
return instance;
@ -104,14 +116,15 @@ public class NPCServerPlayer extends ServerPlayer implements INPCPlayer {
return null;
}
public void onEquipItem(EquipmentSlot p_238393_, ItemStack p_238394_, ItemStack p_238395_) {
@Override
public void onEquipItem(@NotNull EquipmentSlot equipmentSlot, @NotNull ItemStack oldItem, @NotNull ItemStack newItem) {
if (!this.isUsingItem()) {
super.onEquipItem(p_238393_, p_238394_, p_238395_);
super.onEquipItem(equipmentSlot, oldItem, newItem);
}
}
@Override
public void kill() {
this.kill(Messenger.s("Killed"));
}
@ -119,7 +132,9 @@ public class NPCServerPlayer extends ServerPlayer implements INPCPlayer {
public void kill(Component reason) {
this.shakeOff();
this.server.tell(new TickTask(this.server.getTickCount(), () -> {
this.connection.onDisconnect(reason);
if (this.getServer() instanceof IEDGMinecraftServer iedgMinecraftServer) {
iedgMinecraftServer.getNPCPlayerList().delete(this);
}
}));
}
@ -128,10 +143,7 @@ public class NPCServerPlayer extends ServerPlayer implements INPCPlayer {
this.stopRiding();
}
Iterator var1 = this.getIndirectPassengers().iterator();
while(var1.hasNext()) {
Entity passenger = (Entity)var1.next();
for (Entity passenger : this.getIndirectPassengers()) {
if (passenger instanceof Player) {
passenger.stopRiding();
}
@ -139,6 +151,7 @@ public class NPCServerPlayer extends ServerPlayer implements INPCPlayer {
}
@Override
public void die(@NotNull DamageSource damageSource) {
this.shakeOff();
super.die(damageSource);
@ -150,6 +163,11 @@ public class NPCServerPlayer extends ServerPlayer implements INPCPlayer {
@Override
public void addAdditionalSaveData(@NotNull CompoundTag compound) {
super.addAdditionalSaveData(compound);
String npcName = getName().getString();
String npcNameWithoutPrefix = npcName.substring(PREFIX.length());
NBTWriter.of(compound)
.string("NpcName", npcNameWithoutPrefix)
.compound("NpcGameProfile", NbtUtils.writeGameProfile(new CompoundTag(), this.getGameProfile()));
}
@Override
@ -164,7 +182,8 @@ public class NPCServerPlayer extends ServerPlayer implements INPCPlayer {
@Override
public @NotNull Component getName() {
return Component.literal("[NPC]").append(super.getName());
return Component.literal(PREFIX).append(super.getName());
}
@Override
@ -182,10 +201,11 @@ public class NPCServerPlayer extends ServerPlayer implements INPCPlayer {
}
@Override
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);
ServerboundClientCommandPacket packet = new ServerboundClientCommandPacket(ServerboundClientCommandPacket.Action.PERFORM_RESPAWN);
this.connection.handleClientCommand(packet);
}
@ -196,8 +216,16 @@ public class NPCServerPlayer extends ServerPlayer implements INPCPlayer {
return this.connection.player;
}
@Override
protected void checkFallDamage(double y, boolean onGround, @NotNull BlockState state, @NotNull BlockPos pos) {
this.doCheckFallDamage(this.getX(), y, this.getZ(), onGround);
}
@Override
public @NotNull Packet<ClientGamePacketListener> getAddEntityPacket() {
ClientboundAddPlayerPacket clientboundAddPlayerPacket = new ClientboundAddPlayerPacket(this);
clientboundAddPlayerPacket.playerId = getUUID();
return clientboundAddPlayerPacket;
}
}

View File

@ -96,7 +96,7 @@ public class SuperLeadRopeCompat implements IForgeCompat {
}
@OnlyIn(Dist.CLIENT)
private void onRegisterKeyBind(RegisterKeyMappingsEvent event) {
private void onRegisterKeyBind(@NotNull RegisterKeyMappingsEvent event) {
event.register(EDGKeyBindings.KEY_BIND);
}

View File

@ -19,6 +19,7 @@ package top.r3944realms.eroticdungeongame.core.event;
import com.google.common.collect.ImmutableList;
import net.minecraft.client.RecipeBookCategories;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.entity.npc.VillagerTrades;
import net.minecraft.world.entity.player.Player;
import net.minecraftforge.api.distmarker.Dist;
@ -30,15 +31,18 @@ import net.minecraftforge.event.AttachCapabilitiesEvent;
import net.minecraftforge.event.RegisterCommandsEvent;
import net.minecraftforge.event.TickEvent;
import net.minecraftforge.event.entity.player.PlayerEvent;
import net.minecraftforge.event.server.ServerStartedEvent;
import net.minecraftforge.eventbus.api.IEventBus;
import net.minecraftforge.eventbus.api.SubscribeEvent;
import net.minecraftforge.fml.DistExecutor;
import net.minecraftforge.fml.event.lifecycle.FMLCommonSetupEvent;
import net.minecraftforge.fml.event.lifecycle.FMLConstructModEvent;
import net.minecraftforge.fml.javafmlmod.FMLJavaModLoadingContext;
import org.jetbrains.annotations.NotNull;
import top.r3944realms.eroticdungeongame.EroticDungeon;
import top.r3944realms.eroticdungeongame.content.EDGVillagerTrades;
import top.r3944realms.eroticdungeongame.content.command.EDGCommand;
import top.r3944realms.eroticdungeongame.content.entity.npc.INPCPlayer;
import top.r3944realms.eroticdungeongame.content.recipe.DungeonCraftingBookCategory;
import top.r3944realms.eroticdungeongame.content.recipe.DungeonRecipe;
import top.r3944realms.eroticdungeongame.content.recipe.EDGRecipeBookTypes;
@ -46,11 +50,14 @@ import top.r3944realms.eroticdungeongame.content.recipe.EDGRecipeTypeCategories;
import top.r3944realms.eroticdungeongame.core.capability.DungeonDataSyncManager;
import top.r3944realms.eroticdungeongame.core.capability.PlayerDungeonDataProvider;
import top.r3944realms.eroticdungeongame.core.compat.*;
import top.r3944realms.eroticdungeongame.core.network.EDGNetworkHandler;
import top.r3944realms.eroticdungeongame.core.network.toClient.NPCInfoUpdatePacket;
import top.r3944realms.eroticdungeongame.core.register.EDGCapabilities;
import top.r3944realms.eroticdungeongame.core.register.EDGRecipeTypes;
import top.r3944realms.eroticdungeongame.datagen.EDGDataGenEvent;
import top.r3944realms.eroticdungeongame.util.IEDGMinecraftServer;
import top.r3944realms.lib39.api.event.SyncManagerRegisterEvent;
import top.r3944realms.lib39.core.compat.CompatManager;
import top.r3944realms.lib39.core.compat.ForgeCompatManager;
import top.r3944realms.lib39.core.event.CommonEventHandler;
import java.util.List;
@ -65,6 +72,13 @@ public class CommonHandler {
EDGCapabilities.attachCapability(event);
}
@SubscribeEvent
public static void onServerStarted(@NotNull ServerStartedEvent event) {
if (event.getServer() instanceof IEDGMinecraftServer iedgMinecraftServer) {
iedgMinecraftServer.getNPCPlayerList().loadAllNPCs();
}
}
@SubscribeEvent
public static void syncCapabilities(SyncManagerRegisterEvent event) {
dungeonDataSyncManager = new DungeonDataSyncManager();
@ -87,6 +101,11 @@ public class CommonHandler {
Player player = event.getEntity();
if (player == null || player.level().isClientSide()) return;
DungeonDataSyncManager.login(player);
if (player.getServer() instanceof IEDGMinecraftServer iedgMinecraftServer && player instanceof ServerPlayer serverPlayer && !(player instanceof INPCPlayer)) {
EDGNetworkHandler.sendToPlayer(NPCInfoUpdatePacket.createNPCPlayerInitializing(iedgMinecraftServer.getNPCPlayerList().getPlayers()), serverPlayer);
serverPlayer.serverLevel().getChunkSource().removeEntity();
}
}
@SubscribeEvent
@ -108,7 +127,7 @@ public class CommonHandler {
*
* @return the compat manager
*/
public static CompatManager getOrCreateCompatManager() {
public static ForgeCompatManager getOrCreateCompatManager() {
if (compatManager == null) {
synchronized (CommonEventHandler.Mod.class) {
if (compatManager == null) {
@ -122,7 +141,7 @@ public class CommonHandler {
/**
* The Compat manager.
*/
static volatile CompatManager compatManager;
static volatile ForgeCompatManager compatManager;
/**
@ -133,7 +152,7 @@ public class CommonHandler {
@SubscribeEvent
public static void onConstructMod(FMLConstructModEvent event) {
event.enqueueWork(() -> {
CompatManager orCreateCompatManager = Mod.getOrCreateCompatManager();
ForgeCompatManager orCreateCompatManager = Mod.getOrCreateCompatManager();
orCreateCompatManager
.registerCompat(CarryOnCompat.ID, CarryOnCompat.INSTANCE);
orCreateCompatManager
@ -142,16 +161,16 @@ public class CommonHandler {
.registerCompat(EmoteCraftCompat.ID, EmoteCraftCompat.INSTANCE);
orCreateCompatManager
.registerCompat(TouhouLittleMaidCompat.ID, TouhouLittleMaidCompat.INSTANCE);
DistExecutor.unsafeRunWhenOn(Dist.CLIENT, () -> () -> {
orCreateCompatManager
.registerCompat(FreecamCompat.ID, FreecamCompat.INSTANCE);
orCreateCompatManager
.registerCompat(RealCameraCompat.ID, RealCameraCompat.INSTANCE);
orCreateCompatManager
.registerCompat(FirstPersonModelAndRealCameraCompat.ID, FirstPersonModelAndRealCameraCompat.INSTANCE);
}
);
orCreateCompatManager.initialize();
DistExecutor.unsafeRunWhenOn(Dist.CLIENT, () -> () -> {
orCreateCompatManager
.registerCompat(FreecamCompat.ID, FreecamCompat.INSTANCE);
orCreateCompatManager
.registerCompat(RealCameraCompat.ID, RealCameraCompat.INSTANCE);
orCreateCompatManager
.registerCompat(FirstPersonModelAndRealCameraCompat.ID, FirstPersonModelAndRealCameraCompat.INSTANCE);
}
);
orCreateCompatManager.initialize();
});
}
@SubscribeEvent

View File

@ -16,7 +16,6 @@
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;
@ -26,7 +25,6 @@ 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;
@ -63,7 +61,7 @@ public final class NPCInfoUpdatePacket {
}
public NPCInfoUpdatePacket(@NotNull FriendlyByteBuf buf) {
this.actions = EnumSet.noneOf(Action.class);
this.actions = buf.readEnumSet(Action.class);
this.entries = buf.readList((read) -> {
EntryBuilder builder = new EntryBuilder(read.readUUID());
@ -77,7 +75,7 @@ public final class NPCInfoUpdatePacket {
public void write(@NotNull FriendlyByteBuf buffer) {
buffer.writeEnumSet(this.actions, Action.class);
buffer.writeCollection(this.entries, (buf, entry) -> {
buf.writeUUID(entry.profileId());
buf.writeUUID(entry.uuid());
for(Action action : this.actions) {
action.writer.write(buf, entry);
@ -97,12 +95,12 @@ public final class NPCInfoUpdatePacket {
if (clientPacketListener instanceof IEDGClientPacketListener iedgClientPacketListener) {
for (Entry entry : packet.newEntries()) {
PlayerInfo npcInfo = new PlayerInfo(entry.profile, false);
iedgClientPacketListener.getNPCPlayerInfoMap().putIfAbsent(entry.profileId(), npcInfo);
iedgClientPacketListener.getNPCPlayerInfoMap().putIfAbsent(entry.uuid(), npcInfo);
}
for (Entry entry : packet.entries) {
PlayerInfo npcInfo = iedgClientPacketListener.getNPCPlayerInfoMap().get(entry.profileId);
PlayerInfo npcInfo = iedgClientPacketListener.getNPCPlayerInfoMap().get(entry.uuid);
if (npcInfo == null) {
EroticDungeon.getLogger().warn("Ignoring npc player info update for unknown npc player {}", entry.profileId());
EroticDungeon.getLogger().warn("Ignoring npc player info update for unknown npc player {}", entry.uuid());
} else {
for(Action action : packet.actions()) {
applyPlayerInfoUpdate(action, entry, npcInfo);
@ -121,13 +119,13 @@ public final class NPCInfoUpdatePacket {
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
entry.uuid, 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())) {
if (playerInfo.getGameMode() != entry.gameMode() && minecraft.player != null && minecraft.player.getUUID().equals(entry.uuid())) {
minecraft.player.onGameModeChanged(entry.gameMode());
}
@ -204,7 +202,7 @@ public final class NPCInfoUpdatePacket {
}
public record Entry(UUID profileId, GameProfile profile, boolean listed, int latency, GameType gameMode,
public record Entry(UUID uuid, 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));

View File

@ -30,6 +30,9 @@ import top.r3944realms.eroticdungeongame.content.entity.npc.NPCServerPlayer;
import javax.annotation.Nullable;
import java.io.File;
import java.util.*;
import java.util.function.Function;
import java.util.function.Supplier;
public class NPCDataStorage {
private static final Logger LOGGER = LogUtils.getLogger();
@ -41,6 +44,65 @@ public class NPCDataStorage {
this.npcDir = levelStorageAccess.getLevelPath(NPC_DATA_DIR).toFile();
this.npcDir.mkdirs();
}
/**
* 删除指定 NPC 的数据文件
* @param npcUUID 要删除的 NPC UUID
* @return 是否删除成功
*/
public boolean deleteNPCData(UUID npcUUID) {
if (npcUUID == null) {
LOGGER.warn("Cannot delete NPC data: UUID is null");
return false;
}
try {
File dataFile = new File(this.npcDir, npcUUID + ".dat");
File oldFile = new File(this.npcDir, npcUUID + ".dat_old");
boolean deleted = false;
// 删除主数据文件
if (dataFile.exists()) {
deleted = dataFile.delete();
if (deleted) {
LOGGER.info("Deleted NPC data file: {}", dataFile.getName());
} else {
LOGGER.warn("Failed to delete NPC data file: {}", dataFile.getName());
}
}
// 删除备份文件
if (oldFile.exists()) {
boolean oldDeleted = oldFile.delete();
if (oldDeleted) {
LOGGER.debug("Deleted NPC backup file: {}", oldFile.getName());
}
}
return deleted;
} catch (Exception e) {
LOGGER.error("Failed to delete NPC data for UUID: {}", npcUUID, e);
return false;
}
}
/**
* 删除指定 NPC 的数据文件通过 NPCServerPlayer 对象
* @param npc 要删除的 NPC
* @return 是否删除成功
*/
public boolean deleteNPCData(NPCServerPlayer npc) {
if (npc == null) {
LOGGER.warn("Cannot delete NPC data: NPC is null");
return false;
}
return deleteNPCData(npc.getUUID());
}
public void save(NPCServerPlayer player) {
try {
CompoundTag compoundtag = player.saveWithoutId(new CompoundTag());
@ -55,7 +117,27 @@ public class NPCDataStorage {
}
}
@Nullable
public NPCServerPlayer load(UUID uuid, Function<CompoundTag, NPCServerPlayer> loader) {
CompoundTag compoundtag = null;
try {
File file1 = new File(this.npcDir, uuid + ".dat");
if (file1.exists() && file1.isFile()) {
compoundtag = NbtIo.readCompressed(file1);
}
} catch (Exception exception) {
LOGGER.warn("Failed to load player data for {}", uuid, exception);
}
NPCServerPlayer npcServerPlayer = loader.apply(compoundtag);
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, uuid.toString());
return npcServerPlayer;
}
@Nullable
public CompoundTag load(NPCServerPlayer npcServerPlayer) {
CompoundTag compoundtag = null;
@ -73,23 +155,24 @@ public class NPCDataStorage {
int i = NbtUtils.getDataVersion(compoundtag, -1);
npcServerPlayer.load(DataFixTypes.PLAYER.updateToCurrentVersion(this.fixerUpper, compoundtag, i));
}
// net.minecraftforge.event.ForgeEventFactory.firePlayerLoadingEvent(npcServerPlayer, npcDir, npcServerPlayer.getStringUUID());
net.minecraftforge.event.ForgeEventFactory.firePlayerLoadingEvent(npcServerPlayer, npcDir, npcServerPlayer.getStringUUID());
return compoundtag;
}
public String[] getSeenNPCs() {
String[] astring = this.npcDir.list();
Set<String> ret = new HashSet<>();
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);
} else {
for (String s : astring) {
if (s.endsWith(".dat") && !s.endsWith(".dat_old")) {
ret.add(s.substring(0, s.length() - 4));
}
}
astring = ret.toArray(new String[0]);
}
return astring;
}

View File

@ -19,8 +19,10 @@ 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 com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
import it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet;
import net.minecraft.client.Minecraft;
import net.minecraft.client.multiplayer.ClientLevel;
import net.minecraft.client.multiplayer.ClientPacketListener;
import net.minecraft.client.multiplayer.PlayerInfo;
import net.minecraft.network.chat.Component;
@ -28,6 +30,7 @@ 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.jetbrains.annotations.NotNull;
import org.spongepowered.asm.mixin.Final;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
@ -39,6 +42,7 @@ 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.content.entity.npc.NPCRemotePlayer;
import top.r3944realms.eroticdungeongame.util.IEDGClientPacketListener;
import javax.annotation.Nullable;
@ -61,6 +65,7 @@ public abstract class MixinClientPacketListener implements IEDGClientPacketListe
@Shadow @Nullable public abstract PlayerInfo getPlayerInfo(UUID uniqueId);
@Shadow @Final private Set<PlayerInfo> listedPlayers;
@Shadow private ClientLevel level;
@Unique
private Entity edg$cachedMount = null;
@ -84,15 +89,51 @@ public abstract class MixinClientPacketListener implements IEDGClientPacketListe
PacketUtils.ensureRunningOnSameThread(packet, ClientPacketListener.class.cast(this), this.minecraft);
PlayerInfo playerinfo = this.getPlayerInfo(packet.getPlayerId());
if (playerinfo == null) {
PlayerInfo npcPlayerInfo = getNPCPlayerInfo(packet.getPlayerId());
if (npcPlayerInfo == null) {
original.call(packet);
} else {
double d0 = packet.getX();
double d1 = packet.getY();
double d2 = packet.getZ();
float f = (float)(packet.getyRot() * 360) / 256.0F;
float f1 = (float)(packet.getxRot() * 360) / 256.0F;
int i = packet.getEntityId();
assert this.minecraft.level != null;
NPCRemotePlayer npcRemotePlayer = new NPCRemotePlayer(this.minecraft.level, npcPlayerInfo.getProfile());
npcRemotePlayer.setUUID(packet.getPlayerId());
npcRemotePlayer.setId(i);
npcRemotePlayer.syncPacketPositionCodec(d0, d1, d2);
npcRemotePlayer.absMoveTo(d0, d1, d2, f, f1);
npcRemotePlayer.setOldPosAndRot();
this.level.addPlayer(i, npcRemotePlayer);
}
} else original.call(packet);
}
@SuppressWarnings({"unchecked", "SuspiciousMethodCalls"})
@WrapOperation(
method = "handlePlayerInfoUpdate",
at = @At(
value = "INVOKE",
target = "Ljava/util/Map;get(Ljava/lang/Object;)Ljava/lang/Object;"
)
)
private<V> V edg$handlePlayerInfoUpdate(Map<?,?> instance, Object object, @NotNull Operation<V> original) {
V call = original.call(instance, object);
if (call == null) {
call = (V) getNPCPlayerInfoMap().get(object);
}
return call;
}
@SuppressWarnings("AddedMixinMembersNamePattern")
@Override
public Map<UUID, PlayerInfo> getNPCPlayerInfoMap() {
return edg$npcPlayerInfoMap;
}
@SuppressWarnings("AddedMixinMembersNamePattern")
@Override
public Set<PlayerInfo> getListedNPCPlayers() {
return listedPlayers;

View File

@ -0,0 +1,42 @@
/*
* 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.server.dedicated.DedicatedServer;
import net.minecraft.server.players.PlayerList;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import top.r3944realms.eroticdungeongame.content.entity.npc.NPCPlayerList;
import top.r3944realms.eroticdungeongame.util.IEDGMinecraftServer;
@Mixin(DedicatedServer.class)
public abstract class MixinDedicatedServer implements IEDGMinecraftServer {
@WrapOperation(
method = "initServer",
at = @At(
value = "INVOKE",
target = "Lnet/minecraft/server/dedicated/DedicatedServer;setPlayerList(Lnet/minecraft/server/players/PlayerList;)V"
)
)
void edg$initServer$setNPCList(DedicatedServer instance, PlayerList playerList, Operation<Void> original) {
original.call(instance, playerList);
this.setNPCPlayerList(new NPCPlayerList(instance, instance.registries(), this.getNPCDataStorage()));
}
}

View File

@ -0,0 +1,63 @@
/*
* 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.wrapmethod.WrapMethod;
import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
import net.minecraft.commands.CommandSourceStack;
import net.minecraft.commands.arguments.selector.EntitySelector;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.entity.Entity;
import org.jetbrains.annotations.NotNull;
import org.spongepowered.asm.mixin.Final;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import top.r3944realms.eroticdungeongame.content.entity.npc.NPCPlayerList;
import top.r3944realms.eroticdungeongame.content.entity.npc.NPCServerPlayer;
import top.r3944realms.eroticdungeongame.util.IEDGMinecraftServer;
import javax.annotation.Nullable;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.UUID;
import java.util.function.Predicate;
@Mixin(EntitySelector.class)
public class MixinEntitySelector {
@Shadow @Final private Predicate<Entity> predicate;
@Shadow @Final @Nullable private UUID entityUUID;
@WrapMethod(
method = "findEntitiesRaw"
)
private @NotNull List<? extends Entity> edg$findEntitiesRaw$findNPCs(CommandSourceStack source, @NotNull Operation<List<? extends Entity>> original) {
List<Entity> ret = new ArrayList<>(original.call(source));
if (source.getServer() instanceof IEDGMinecraftServer iedgMinecraftServer) {
NPCPlayerList npcPlayerList = iedgMinecraftServer.getNPCPlayerList();
if (npcPlayerList != null) {
for (NPCServerPlayer npc : npcPlayerList.getPlayers()) {
if (Objects.equals(entityUUID, npc.getUUID()) && predicate.test(npc)) {
ret.add(npc);
}
}
}
}
return ret;
}
}

View File

@ -0,0 +1,41 @@
/*
* 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.gametest.framework.GameTestServer;
import net.minecraft.server.players.PlayerList;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import top.r3944realms.eroticdungeongame.content.entity.npc.NPCPlayerList;
import top.r3944realms.eroticdungeongame.util.IEDGMinecraftServer;
@Mixin(GameTestServer.class)
public abstract class MixinGameTestServer implements IEDGMinecraftServer {
@WrapOperation(
method = "initServer",
at = @At(
value = "HEAD",
target = "Lnet/minecraft/gametest/framework/GameTestServer;setPlayerList(Lnet/minecraft/server/players/PlayerList;)V"
)
)
void edg$initServer$setNPCList(GameTestServer instance, PlayerList playerList, Operation<Void> original) {
original.call(instance, playerList);
this.setNPCPlayerList(new NPCPlayerList(instance, instance.registries(), this.getNPCDataStorage()));
}
}

View File

@ -0,0 +1,67 @@
/*
* 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.google.common.collect.Lists;
import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
import net.minecraft.client.server.IntegratedServer;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.server.players.PlayerList;
import org.jetbrains.annotations.NotNull;
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 top.r3944realms.eroticdungeongame.content.entity.npc.NPCPlayerList;
import top.r3944realms.eroticdungeongame.content.entity.npc.NPCServerPlayer;
import top.r3944realms.eroticdungeongame.util.IEDGMinecraftServer;
import javax.annotation.Nullable;
import java.util.UUID;
@Mixin(IntegratedServer.class)
public abstract class MixinIntegratedServer implements IEDGMinecraftServer {
@Shadow @Nullable private UUID uuid;
@Inject(
method = "<init>",
at = @At("TAIL")
)
private void edg$init(CallbackInfo ci) {
IntegratedServer server = IntegratedServer.class.cast(this);
setNPCPlayerList(new NPCPlayerList(server, server.registries(), getNPCDataStorage()));
}
@WrapOperation(
method = "halt",
at = @At(
value = "INVOKE",
target = "Lnet/minecraft/client/server/IntegratedServer;executeBlocking(Ljava/lang/Runnable;)V"
)
)
private void edg$halt$3$removeNpc(IntegratedServer instance, Runnable runnable, @NotNull Operation<Void> original) {
original.call(instance, runnable);
original.call(instance, (Runnable) () -> {
for(NPCServerPlayer npcServerPlayer : Lists.newArrayList(getNPCPlayerList().getPlayers())) {
if (!npcServerPlayer.getUUID().equals(this.uuid)) {
getNPCPlayerList().remove(npcServerPlayer);
}
}
});
}
}

View File

@ -0,0 +1,37 @@
/*
* 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 net.minecraft.util.datafix.DataFixers;
import net.minecraft.world.level.storage.LevelStorageSource;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import top.r3944realms.eroticdungeongame.core.storage.NPCDataStorage;
import top.r3944realms.eroticdungeongame.util.IEDGLevelStorageAccess;
@Mixin(LevelStorageSource.LevelStorageAccess.class)
public abstract class MixinLevelStorageSource$LevelStorageAccess implements IEDGLevelStorageAccess {
@Shadow protected abstract void checkLock();
@SuppressWarnings("AddedMixinMembersNamePattern")
@Override
public NPCDataStorage createNPCPlayerDataStorage() {
this.checkLock();
return new NPCDataStorage(LevelStorageSource.LevelStorageAccess.class.cast(this), DataFixers.getDataFixer());
}
}

View File

@ -18,24 +18,43 @@ 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 com.mojang.datafixers.DataFixer;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.Services;
import net.minecraft.server.WorldStem;
import net.minecraft.server.level.progress.ChunkProgressListenerFactory;
import net.minecraft.server.packs.repository.PackRepository;
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 org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import top.r3944realms.eroticdungeongame.content.entity.npc.NPCPlayerList;
import top.r3944realms.eroticdungeongame.core.storage.NPCDataStorage;
import top.r3944realms.eroticdungeongame.util.IEDGLevelStorageAccess;
import top.r3944realms.eroticdungeongame.util.IEDGMinecraftServer;
import java.net.Proxy;
@Mixin(MinecraftServer.class)
public class MixinMinecraftServer implements IEDGMinecraftServer {
@Unique
private NPCDataStorage edg$npcDataStorage;
@Unique
private NPCPlayerList edg$npcPlayerList;
@Inject(
method = "<init>",
at = @At("TAIL")
)
private void edg$init(Thread serverThread, LevelStorageSource.LevelStorageAccess storageSource, PackRepository packRepository, WorldStem worldStem, Proxy proxy, DataFixer fixerUpper, Services services, ChunkProgressListenerFactory progressListenerFactory, CallbackInfo ci) {
if (storageSource instanceof IEDGLevelStorageAccess iedgLevelStorageAccess) {
edg$npcDataStorage = iedgLevelStorageAccess.createNPCPlayerDataStorage();
}
}
@SuppressWarnings("AddedMixinMembersNamePattern")
@Override
@ -48,6 +67,13 @@ public class MixinMinecraftServer implements IEDGMinecraftServer {
public void setNPCPlayerList(NPCPlayerList playerList) {
this.edg$npcPlayerList = playerList;
}
@SuppressWarnings("AddedMixinMembersNamePattern")
@Override
public NPCDataStorage getNPCDataStorage() {
return edg$npcDataStorage;
}
@WrapOperation(
method = "saveEverything",
at = @At(
@ -84,5 +110,17 @@ public class MixinMinecraftServer implements IEDGMinecraftServer {
edg$npcPlayerList.saveAll();
edg$npcPlayerList.removeAll();
}
@WrapOperation(
method = "tickChildren",
at = @At(
value = "INVOKE",
target = "Lnet/minecraft/server/players/PlayerList;tick()V"
)
)
void edg$tickChildren$tick(PlayerList instance, @NotNull Operation<Void> original) {
original.call(instance);
edg$npcPlayerList.tick();
}
}

View File

@ -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.util;
public interface IEDGEntitySelector {
}

View File

@ -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 top.r3944realms.eroticdungeongame.core.storage.NPCDataStorage;
public interface IEDGLevelStorageAccess {
NPCDataStorage createNPCPlayerDataStorage();
}

View File

@ -17,8 +17,10 @@
package top.r3944realms.eroticdungeongame.util;
import top.r3944realms.eroticdungeongame.content.entity.npc.NPCPlayerList;
import top.r3944realms.eroticdungeongame.core.storage.NPCDataStorage;
public interface IEDGMinecraftServer {
NPCPlayerList getNPCPlayerList();
void setNPCPlayerList(NPCPlayerList playerList);
NPCDataStorage getNPCDataStorage();
}

View File

@ -8,11 +8,14 @@
"mixins": [
"bendylib.MixinBendableCuboidBuilder",
"minecraft.MixinClientConnect",
"minecraft.MixinDedicatedServer",
"minecraft.MixinEntity",
"minecraft.MixinEntitySelector",
"minecraft.MixinGameTestServer",
"minecraft.MixinLevelStorageSource$LevelStorageAccess",
"minecraft.MixinLivingEntity",
"minecraft.MixinMinecraftServer",
"minecraft.MixinPlayer",
"minecraft.MixinServerLoginPacketListenerImpl",
"minecraft.MixinServerPlayer"
],
"client": [
@ -20,6 +23,7 @@
"minecraft.MixinClientPacketListener",
"minecraft.MixinEntityRenderer",
"minecraft.MixinGameRender",
"minecraft.MixinIntegratedServer",
"minecraft.MixinItemInHandRenderer",
"minecraft.MixinMinecraft",
"minecraft.MixinPlayerRenderer"

View File

@ -45,4 +45,5 @@ public net.minecraft.client.multiplayer.ClientPacketListener m_245842_(Lnet/mine
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
public net.minecraft.client.multiplayer.ClientPacketListener f_104892_ # playerInfoMap
public-f net.minecraft.network.protocol.game.ClientboundAddPlayerPacket f_131588_ # playerId