feat: nPC

This commit is contained in:
叁玖领域 2026-03-20 00:25:06 +08:00
parent cd5689b4a6
commit f1f5e204a2
28 changed files with 1503 additions and 22 deletions

View File

@ -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<CommandSourceStack> 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<CommandSourceStack> context) throws CommandSyntaxException {
//todo: 完善下内容
CommandSourceStack source = context.getSource();

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.content.entity.npc;
public interface INPCPlayer {
}

View File

@ -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<RegistryLayer> registries;
private final RegistryAccess.Frozen synchronizedRegistries;
private final NPCDataStorage npcIo;
private final MinecraftServer server;
private final List<NPCServerPlayer> npcs = new ArrayList<>();
private final Map<UUID, NPCServerPlayer> npcsByUUID = Maps.newHashMap();
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) {
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<GameProfile> 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<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;
}
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<Objective> 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<ServerPlayer, Component> 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<ServerPlayer> 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<Level> 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<NPCServerPlayer> 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;
}
}

View File

@ -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);
}
}

View File

@ -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<Level> 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<GameProfile> 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);
}
}

View File

@ -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<Void> 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 <MSG> void sendToPlayer(MSG message, ServerPlayer player){
CHANNEL.send(PacketDistributor.PLAYER.with(() -> player), message);
}
public static <MSG> void sendToAllPlayer(MSG message){
CHANNEL.send(ALL_PLAYER.noArg(), message);
}
}

View File

@ -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() {
}
}

View File

@ -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<UUID> 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<NetworkEvent.Context> 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);
}
}

View File

@ -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<Action> actions;
private final List<Entry> entries;
public List<Entry> newEntries() {
return this.actions.contains(Action.ADD_NPC) ? this.entries : List.of();
}
public NPCInfoUpdatePacket(EnumSet<Action> actions, List<Entry> 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<Action> actions, @NotNull Collection<NPCServerPlayer> 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<NPCServerPlayer> players) {
EnumSet<Action> 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<NetworkEvent.Context> 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<Action> actions() {
return actions;
}
public List<Entry> 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);
}
}
}

View File

@ -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;
}
}

View File

@ -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<BlockHitResult> original) {
private BlockHitResult edg$getMaxZoomFix(BlockGetter instance, ClipContext context, Operation<BlockHitResult> original) {
return Services.WORK_SPACE.tryToDoIfInDeviceAndRet(
Minecraft.getInstance().player,
data -> {

View File

@ -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);
}

View File

@ -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<UUID, PlayerInfo> edg$npcPlayerInfoMap = Maps.newHashMap();
@Unique
private final Set<PlayerInfo> 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<PlayerInfo> 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<Void> 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<UUID, PlayerInfo> getNPCPlayerInfoMap() {
return edg$npcPlayerInfoMap;
}
@Override
public Set<PlayerInfo> getListedNPCPlayers() {
return listedPlayers;
}
}

View File

@ -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<AABB> original){
private AABB edg$redefinedBoundingBox(@NotNull Operation<AABB> 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<Boolean> original) {
private boolean edg$redefineStartRiding(Entity vehicle, boolean force, Operation<Boolean> 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<Void> original) {
private void edg$redefineStopRiding(Operation<Void> 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<Boolean> original) {
if (this.getControllingPassenger() instanceof NPCServerPlayer) {
return !this.level.isClientSide;
}
return original.call();
}
@SuppressWarnings("AddedMixinMembersNamePattern")

View File

@ -28,7 +28,7 @@ import top.r3944realms.eroticdungeongame.api.workspace.Services;
@Mixin(EntityRenderer.class)
public abstract class MixinEntityRenderer<T extends Entity> {
@WrapMethod(method = "getSkyLightLevel")
protected int getSkyLightLevel(T entity, BlockPos pos, Operation<Integer> original) {
protected int edg$getSkyLightLevel(T entity, BlockPos pos, Operation<Integer> original) {
return Services.WORK_SPACE.tryToDoIfInDeviceAndRet(
entity,
data -> {

View File

@ -35,7 +35,7 @@ public class MixinGameRender {
@WrapMethod(
method = "renderItemInHand"
)
private void warpRenderHand(PoseStack poseStack, Camera activeRenderInfo, float partialTicks, Operation<Void> original) {
private void edg$warpRenderHand(PoseStack poseStack, Camera activeRenderInfo, float partialTicks, Operation<Void> original) {
Services.WORK_SPACE.tryToDoIfInDevice(minecraft.player, data -> {}, () -> original.call(poseStack, activeRenderInfo, partialTicks));
}
}

View File

@ -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<Void> original) {
Services.WORK_SPACE.tryToDoIfInDevice(entity, data -> {
if(data.getDeviceMainBlockPos() != null && !(entity.level().getBlockState(data.getDeviceMainBlockPos()).getBlock() instanceof DisplayRackBlock)) {

View File

@ -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<Boolean> original) {
private boolean edg$inWall$Warp(LivingEntity instance, Operation<Boolean> original) {
return Services.WORK_SPACE.tryToDoIfInDeviceAndRet(instance, data -> false, () -> original.call(instance));
}
@WrapMethod(
method = "stopRiding"
)
private void redefineStopRiding(Operation<Void> original) {
private void edg$redefineStopRiding(Operation<Void> original) {
LivingEntity livingEntity = LivingEntity.class.cast(this);
Services.WORK_SPACE.tryToDoIfInDevice(livingEntity,
data -> {

View File

@ -35,21 +35,21 @@ public class MixinMinecraft {
@WrapMethod(
method = "startAttack"
)
private boolean forbiddenAttack(Operation<Boolean> original){
private boolean edg$forbiddenAttack(Operation<Boolean> original){
return Services.WORK_SPACE.tryToDoIfInDeviceAndRet(player, data -> false, original::call);
}
@WrapMethod(
method = "startUseItem"
)
private void forbiddenUseItem(Operation<Void> original){
private void edg$forbiddenUseItem(Operation<Void> original){
Services.WORK_SPACE.tryToDoIfInDevice(player, data -> {}, original::call);
}
@WrapMethod(
method = "continueAttack"
)
private void forbiddenContinueAttack(boolean leftClick, Operation<Void> original){
private void edg$forbiddenContinueAttack(boolean leftClick, Operation<Void> original){
Services.WORK_SPACE.tryToDoIfInDeviceAndRet(player, data -> false, () -> original.call(leftClick));
}
}

View File

@ -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<Void> 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<Void> 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<Void> original) {
original.call(instance);
edg$npcPlayerList.saveAll();
edg$npcPlayerList.removeAll();
}
}

View File

@ -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<? extends LivingEntity> entityType, Level level) {
super(entityType, level);
}
@Redirect(
method = "<init>",
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<Boolean> original) {
public boolean edg$shouldShowName(@NotNull Operation<Boolean> original) {
Player player = Player.class.cast(this);
return Services.WORK_SPACE.tryToDoIfInDeviceAndRet(player, data -> false, original::call);
}
@WrapMethod(method = "wantsToStopRiding")
private boolean wantToStopRide(Operation<Boolean> original) {
private boolean edg$wantToStopRide(@NotNull Operation<Boolean> 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<Void> original) {
if (!(instance instanceof INPCPlayer)) {
original.call(instance, uuid);
}
}
}

View File

@ -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<Void> original) {
private void edg$renderNameTag(AbstractClientPlayer entity, Component displayName, PoseStack poseStack, MultiBufferSource buffer, int packedLight, Operation<Void> original) {
Services.WORK_SPACE.tryToDoIfInDevice(entity, data -> {}, () -> original.call(entity, displayName, poseStack, buffer, packedLight));
}
@ -50,7 +50,7 @@ public class MixinPlayerRenderer {
)
)
private <T extends LivingEntity> void renderFix(PlayerRenderer instance, T entity, float entityYaw, float partialTicks, PoseStack poseStack, MultiBufferSource buffer, int packedLight, Operation<Void> original) {
private <T extends LivingEntity> void edg$renderFix(PlayerRenderer instance, T entity, float entityYaw, float partialTicks, PoseStack poseStack, MultiBufferSource buffer, int packedLight, Operation<Void> original) {
Services.WORK_SPACE.tryToDoIfInDevice(entity, data -> {
Minecraft minecraft = Minecraft.getInstance();
if(!(Objects.equals(minecraft.player, entity) && minecraft.options.getCameraType().isFirstPerson() &&

View File

@ -57,7 +57,7 @@ public abstract class MixinServerPlayer extends Player implements IEDGEntity {
@WrapMethod(
method = "stopRiding"
)
private void redefineStopRiding(Operation<Void> original) {
private void edg$redefineStopRiding(Operation<Void> original) {
ServerPlayer player = ServerPlayer.class.cast(this);
Services.WORK_SPACE.tryToDoIfInDevice(player,
data -> {

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 io.netty.channel.Channel;
public interface IClientConnection {
void setChannel(Channel var1);
}

View File

@ -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<UUID, PlayerInfo> getNPCPlayerInfoMap();
Set<PlayerInfo> getListedNPCPlayers();
default Collection<PlayerInfo> getOnlineNPCPlayers() {
return getNPCPlayerInfoMap().values();
}
default Collection<UUID> 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;
}
}

View File

@ -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);
}

View File

@ -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": [

View File

@ -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
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